From 445bc22f58da01ccec37969f550608603ffc85c0 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Sat, 21 Feb 2026 23:01:20 +0000 Subject: [PATCH 01/28] camera branch again I couldn't rebase it --- .gitmodules | 3 + CMakeLists.txt | 2 + dependencies/openpnp-capture | 1 + src/CMakeLists.txt | 1 + src/Cafe/CMakeLists.txt | 1 + src/Cafe/OS/libs/camera/camera.cpp | 407 +++++++++++++------------ src/Cafe/OS/libs/camera/camera.h | 3 - src/camera/CMakeLists.txt | 16 + src/camera/CameraManager.cpp | 228 ++++++++++++++ src/camera/CameraManager.h | 27 ++ src/camera/Rgb2Nv12.cpp | 65 ++++ src/camera/Rgb2Nv12.h | 9 + src/config/CemuConfig.cpp | 5 + src/config/CemuConfig.h | 4 + src/gui/wxgui/CMakeLists.txt | 2 + src/gui/wxgui/CameraSettingsWindow.cpp | 97 ++++++ src/gui/wxgui/CameraSettingsWindow.h | 21 ++ 17 files changed, 702 insertions(+), 190 deletions(-) create mode 160000 dependencies/openpnp-capture create mode 100644 src/camera/CMakeLists.txt create mode 100644 src/camera/CameraManager.cpp create mode 100644 src/camera/CameraManager.h create mode 100644 src/camera/Rgb2Nv12.cpp create mode 100644 src/camera/Rgb2Nv12.h create mode 100644 src/gui/wxgui/CameraSettingsWindow.cpp create mode 100644 src/gui/wxgui/CameraSettingsWindow.h diff --git a/.gitmodules b/.gitmodules index 82e53209..cdc1d77e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,6 @@ [submodule "dependencies/xbyak_aarch64"] path = dependencies/xbyak_aarch64 url = https://github.com/fujitsu/xbyak_aarch64 +[submodule "dependencies/openpnp-capture"] + path = dependencies/openpnp-capture + url = https://github.com/capitalistspz/openpnp-capture-fork diff --git a/CMakeLists.txt b/CMakeLists.txt index a00879fe..43e27dc3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -251,4 +251,6 @@ if (NOT ZArchive_FOUND) add_subdirectory("dependencies/ZArchive" EXCLUDE_FROM_ALL) endif() +add_subdirectory("dependencies/openpnp-capture" EXCLUDE_FROM_ALL SYSTEM) + add_subdirectory(src) diff --git a/dependencies/openpnp-capture b/dependencies/openpnp-capture new file mode 160000 index 00000000..b7765b1d --- /dev/null +++ b/dependencies/openpnp-capture @@ -0,0 +1 @@ +Subproject commit b7765b1da5169c662d141534bb877b3665f11227 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e73d5f1f..4553fe31 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -55,6 +55,7 @@ add_subdirectory(Cemu) add_subdirectory(config) add_subdirectory(input) add_subdirectory(audio) +add_subdirectory(camera) add_subdirectory(util) add_subdirectory(imgui) add_subdirectory(resource) diff --git a/src/Cafe/CMakeLists.txt b/src/Cafe/CMakeLists.txt index 78a8cce0..468a7e74 100644 --- a/src/Cafe/CMakeLists.txt +++ b/src/Cafe/CMakeLists.txt @@ -627,6 +627,7 @@ endif() target_link_libraries(CemuCafe PRIVATE CemuCommon + CemuCamera CemuGui ZArchive::zarchive imguiImpl diff --git a/src/Cafe/OS/libs/camera/camera.cpp b/src/Cafe/OS/libs/camera/camera.cpp index 6812de7b..70cb81bc 100644 --- a/src/Cafe/OS/libs/camera/camera.cpp +++ b/src/Cafe/OS/libs/camera/camera.cpp @@ -5,241 +5,274 @@ #include "Cafe/OS/libs/coreinit/coreinit_Alarm.h" #include "Cafe/OS/libs/coreinit/coreinit_Time.h" #include "Cafe/HW/Espresso/PPCCallback.h" +#include "camera/CameraManager.h" +#include "Common/CafeString.h" +#include "util/helpers/ringbuffer.h" namespace camera { + constexpr unsigned CAMERA_WIDTH = 640; + constexpr unsigned CAMERA_HEIGHT = 480; + + 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 + }; + + enum class CAMFps : uint32 + { + _15 = 0, + _30 = 1 + }; + + enum class CAMEventType : uint32 + { + Decode = 0, + Detached = 1 + }; + + enum class CAMForceDisplay + { + None = 0, + DRC = 1 + }; + + enum class CAMImageType : uint32 + { + Default = 0 + }; + + struct CAMImageInfo + { + betype type; + uint32be height; + uint32be width; + }; + static_assert(sizeof(CAMImageInfo) == 0x0C); struct CAMInitInfo_t { - /* +0x00 */ uint32be ukn00; - /* +0x04 */ uint32be width; - /* +0x08 */ uint32be height; - - /* +0x0C */ uint32be workMemorySize; - /* +0x10 */ MEMPTR workMemory; - - /* +0x14 */ uint32be handlerFuncPtr; - - /* +0x18 */ uint32be ukn18; - /* +0x1C */ uint32be fps; - - /* +0x20 */ uint32be ukn20; + CAMImageInfo imageInfo; + uint32be workMemorySize; + MEMPTR workMemoryData; + MEMPTR callback; + betype forceDisplay; + betype fps; + uint32be threadFlags; + uint8 unk[0x10]; }; + static_assert(sizeof(CAMInitInfo_t) == 0x34); - struct CAMTargetSurface + 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; + sint32be size; + MEMPTR data; + uint8 unused[0x18]; }; + static_assert(sizeof(CAMTargetSurface) == 0x20); - struct CAMCallbackParam + struct CAMDecodeEventParam { - // type 0 - frame decoded | field1 - imagePtr, field2 - imageSize, field3 - ukn (0) - // type 1 - ??? - - - /* +0x0 */ uint32be type; // 0 -> Frame decoded - /* +0x4 */ uint32be field1; - /* +0x8 */ uint32be field2; - /* +0xC */ uint32be field3; + betype type; + MEMPTR data; + uint32be channel; + uint32be errored; }; + static_assert(sizeof(CAMDecodeEventParam) == 0x10); + constexpr static int32_t CAM_HANDLE = 0; - #define CAM_ERROR_SUCCESS 0 - #define CAM_ERROR_INVALID_HANDLE -8 - - 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; - - CameraInstance* GetCameraInstanceByHandle(sint32 camHandle) + struct { - 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]; + std::recursive_mutex mutex{}; + bool initialized = false; + bool shouldTriggerCallback = false; + std::atomic_bool isOpen = false; + std::atomic_bool isExiting = false; + bool isWorking = false; + unsigned fps = 30; + MEMPTR eventCallback = nullptr; + RingBuffer, 20> inTargetBuffers{}; + RingBuffer, 20> outTargetBuffers{}; + } s_instance; + + SysAllocator s_cameraEventData; + SysAllocator s_cameraWorkerThread; + SysAllocator s_cameraWorkerThreadStack; + SysAllocator> s_cameraWorkerThreadNameBuffer; + SysAllocator s_cameraOpenEvent; + + void WorkerThread(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()); + + while (!s_instance.isExiting) + { + coreinit::OSWaitEvent(s_cameraOpenEvent); + while (true) + { + if (!s_instance.isOpen || s_instance.isExiting) + { + // Fill leftover buffers before stopping + if (!s_instance.inTargetBuffers.HasData()) + break; + } + s_cameraEventData->type = CAMEventType::Decode; + s_cameraEventData->channel = 0; + + const auto surfaceBuffer = s_instance.inTargetBuffers.Pop(); + if (surfaceBuffer.IsNull()) + { + s_cameraEventData->data = nullptr; + s_cameraEventData->errored = true; + } + else + { + CameraManager::FillNV12Buffer(surfaceBuffer.GetPtr()); + s_cameraEventData->data = surfaceBuffer; + s_cameraEventData->errored = false; + } + PPCCoreCallback(s_instance.eventCallback, s_cameraEventData.GetMPTR()); + coreinit::OSSleepTicks(Espresso::TIMER_CLOCK / (s_instance.fps - 1)); + } + } + coreinit::OSExitThread(0); } - struct CameraInstance - { - CameraInstance(uint32 frameWidth, uint32 frameHeight, MPTR handlerFunc) : width(frameWidth), height(frameHeight), handlerFunc(handlerFunc) { AcquireHandle(); }; - ~CameraInstance() { if (isOpen) { CloseCam(); } ReleaseHandle(); }; - - sint32 handle{ 0 }; - uint32 width; - uint32 height; - bool isOpen{false}; - std::queue queue_targetSurfaces; - MPTR handlerFunc; - - bool OpenCam() - { - if (isOpen) - return false; - isOpen = true; - g_activeCameraInstances.push_back(this); - return true; - } - - bool CloseCam() - { - if (!isOpen) - return false; - isOpen = false; - vectorRemoveByValue(g_activeCameraInstances, this); - return true; - } - - 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); - } - - 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); - } - - 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); - } - }; - - sint32 CAMGetMemReq(void* ukn) + sint32 CAMGetMemReq(const CAMImageInfo* info) { + if (!info) + return CAM_STATUS_INVALID_ARG; return 1 * 1024; // always return 1KB } - sint32 CAMCheckMemSegmentation(void* base, uint32 size) + CAMStatus CAMCheckMemSegmentation(void* startAddr, uint32 size) { - return CAM_ERROR_SUCCESS; // always return success + if (!startAddr || size == 0) + return CAM_STATUS_INVALID_ARG; + return CAM_STATUS_SUCCESS; } - void ppcCAMUpdate60(PPCInterpreter_t* hCPU) + sint32 CAMInit(uint32 cameraId, const CAMInitInfo_t* initInfo, betype* error) { - // 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()); + *error = CAM_STATUS_DEVICE_IN_USE; + return -1; } - osLib_returnFromFunction(hCPU, 0); - } - - 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) + if (!initInfo || !initInfo->workMemoryData || + !match_any_of(initInfo->forceDisplay, CAMForceDisplay::None, CAMForceDisplay::DRC) || + !match_any_of(initInfo->fps, CAMFps::_15, CAMFps::_30) || + initInfo->imageInfo.type != CAMImageType::Default) { - coreinit::OSCreateAlarm(g_alarm_camera.GetPtr()); - coreinit::OSSetPeriodicAlarm(g_alarm_camera.GetPtr(), coreinit::OSGetTime(), (uint64)ESPRESSO_TIMER_CLOCK / 60ull, RPLLoader_MakePPCCallable(ppcCAMUpdate60)); + *error = CAM_STATUS_INVALID_ARG; + return -1; } - g_cameraCounter++; + CameraManager::Init(); - return camInstance->handle; + cemu_assert_debug(initInfo->forceDisplay != CAMForceDisplay::DRC); + cemu_assert_debug(initInfo->workMemorySize != 0); + cemu_assert_debug(initInfo->imageInfo.type == CAMImageType::Default); + + s_instance.isExiting = false; + s_instance.fps = initInfo->fps == CAMFps::_15 ? 15 : 30; + s_instance.initialized = true; + s_instance.eventCallback = initInfo->callback; + + coreinit::OSInitEvent(s_cameraOpenEvent, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_AUTO); + + coreinit::__OSCreateThreadType( + s_cameraWorkerThread, RPLLoader_MakePPCCallable(WorkerThread), 0, nullptr, + s_cameraWorkerThreadStack.GetPtr() + s_cameraWorkerThreadStack.GetByteSize(), s_cameraWorkerThreadStack.GetByteSize(), + 0x10, initInfo->threadFlags & 7, OSThread_t::THREAD_TYPE::TYPE_DRIVER); + s_cameraWorkerThreadNameBuffer->assign("CameraWorkerThread"); + coreinit::OSSetThreadName(s_cameraWorkerThread.GetPtr(), s_cameraWorkerThreadNameBuffer->c_str()); + coreinit::OSResumeThread(s_cameraWorkerThread.GetPtr()); + return CAM_STATUS_SUCCESS; } - sint32 CAMExit(sint32 camHandle) + CAMStatus CAMClose(sint32 camHandle) { - CameraInstance* camInstance = GetCameraInstanceByHandle(camHandle); - if (!camInstance) - return CAM_ERROR_INVALID_HANDLE; - CAMClose(camHandle); - delete camInstance; - - std::unique_lock _lock(g_mutex_camera); - g_cameraCounter--; - if (g_cameraCounter == 0) - coreinit::OSCancelAlarm(g_alarm_camera.GetPtr()); - return CAM_ERROR_SUCCESS; + if (camHandle != CAM_HANDLE) + return CAM_STATUS_INVALID_HANDLE; + { + std::scoped_lock lock(s_instance.mutex); + if (!s_instance.initialized || !s_instance.isOpen) + return CAM_STATUS_UNINITIALIZED; + s_instance.isOpen = false; + } + CameraManager::Close(); + return CAM_STATUS_SUCCESS; } - sint32 CAMOpen(sint32 camHandle) + CAMStatus CAMOpen(sint32 camHandle) { - CameraInstance* camInstance = GetCameraInstanceByHandle(camHandle); - if (!camInstance) - return CAM_ERROR_INVALID_HANDLE; - camInstance->OpenCam(); - return CAM_ERROR_SUCCESS; + if (camHandle != CAM_HANDLE) + return CAM_STATUS_INVALID_HANDLE; + auto lock = std::scoped_lock(s_instance.mutex); + 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(); + s_instance.outTargetBuffers.Clear(); + return CAM_STATUS_SUCCESS; } - sint32 CAMClose(sint32 camHandle) + CAMStatus CAMSubmitTargetSurface(sint32 camHandle, CAMTargetSurface* targetSurface) { - CameraInstance* camInstance = GetCameraInstanceByHandle(camHandle); - if (!camInstance) - return CAM_ERROR_INVALID_HANDLE; - camInstance->CloseCam(); - return CAM_ERROR_SUCCESS; + if (camHandle != CAM_HANDLE) + return CAM_STATUS_INVALID_HANDLE; + if (!targetSurface || targetSurface->data.IsNull() || targetSurface->size < 1) + return CAM_STATUS_INVALID_ARG; + auto lock = std::scoped_lock(s_instance.mutex); + 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; } - sint32 CAMSubmitTargetSurface(sint32 camHandle, CAMTargetSurface* targetSurface) + void CAMExit(sint32 camHandle) { - CameraInstance* camInstance = GetCameraInstanceByHandle(camHandle); - if (!camInstance) - return CAM_ERROR_INVALID_HANDLE; - - camInstance->QueueTargetSurface(targetSurface); - - return CAM_ERROR_SUCCESS; + if (camHandle != CAM_HANDLE) + return; + std::scoped_lock lock(s_instance.mutex); + if (!s_instance.initialized) + return; + s_instance.isExiting = true; + if (s_instance.isOpen) + CAMClose(camHandle); + coreinit::OSSignalEvent(s_cameraOpenEvent.GetPtr()); + coreinit::OSJoinThread(s_cameraWorkerThread, nullptr); + s_instance.initialized = false; } void reset() { - g_cameraCounter = 0; + CAMExit(0); } class : public COSModule 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/camera/CMakeLists.txt b/src/camera/CMakeLists.txt new file mode 100644 index 00000000..1584eb20 --- /dev/null +++ b/src/camera/CMakeLists.txt @@ -0,0 +1,16 @@ + +add_library(CemuCamera + CameraManager.cpp + CameraManager.h + Rgb2Nv12.cpp + Rgb2Nv12.h + CameraManager.h + CameraManager.cpp + Rgb2Nv12.h + Rgb2Nv12.cpp +) + +set_property(TARGET CemuCamera PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + +target_include_directories(CemuCamera PUBLIC "../") +target_link_libraries(CemuCamera PRIVATE CemuCommon CemuUtil openpnp-capture) diff --git a/src/camera/CameraManager.cpp b/src/camera/CameraManager.cpp new file mode 100644 index 00000000..8fbcbea9 --- /dev/null +++ b/src/camera/CameraManager.cpp @@ -0,0 +1,228 @@ +#include "CameraManager.h" + +#include "config/CemuConfig.h" +#include "util/helpers/helpers.h" +#include "Rgb2Nv12.h" + +#include +#include +#include +#include + +#include + +constexpr unsigned CAMERA_WIDTH = 640; +constexpr unsigned CAMERA_HEIGHT = 480; +constexpr unsigned CAMERA_PITCH = 768; + +namespace CameraManager +{ + std::mutex s_mutex; + CapContext s_ctx; + std::optional s_device; + std::optional s_stream; + std::array s_rgbBuffer; + std::array s_nv12Buffer; + int s_refCount = 0; + std::thread s_captureThread; + std::atomic_bool s_capturing = false; + std::atomic_bool s_running = false; + + std::string FourCC(uint32le value) + { + return { + static_cast((value >> 0) & 0xFF), + static_cast((value >> 8) & 0xFF), + static_cast((value >> 16) & 0xFF), + static_cast((value >> 24) & 0xFF)}; + } + + void CaptureLogFunction(uint32_t level, const char* string) + { + cemuLog_log(LogType::InputAPI, "OpenPNPCapture: {}: {}", level, string); + } + + std::optional FindCorrectFormat() + { + const auto device = *s_device; + cemuLog_log(LogType::InputAPI, "Video capture device '{}' available formats:", Cap_getDeviceName(s_ctx, device)); + const auto formatCount = Cap_getNumFormats(s_ctx, device); + for (int32_t formatId = 0; formatId < formatCount; ++formatId) + { + CapFormatInfo formatInfo; + if (Cap_getFormatInfo(s_ctx, device, formatId, &formatInfo) != CAPRESULT_OK) + continue; + cemuLog_log(LogType::Force, "{}: {} {}x{} @ {} fps, {} bpp", formatId, FourCC(formatInfo.fourcc), formatInfo.width, formatInfo.height, formatInfo.fps, formatInfo.bpp); + if (formatInfo.width == CAMERA_WIDTH && formatInfo.height == CAMERA_HEIGHT) + { + cemuLog_log(LogType::Force, "Selected video format {}", formatId); + return formatId; + } + } + cemuLog_log(LogType::Force, "Failed to find suitable video format"); + return std::nullopt; + } + + void CaptureWorker() + { + SetThreadName("CameraManager"); + while (s_running) + { + while (s_capturing) + { + s_mutex.lock(); + if (s_stream && Cap_hasNewFrame(s_ctx, *s_stream) && + Cap_captureFrame(s_ctx, *s_stream, s_rgbBuffer.data(), s_rgbBuffer.size()) == CAPRESULT_OK) + Rgb2Nv12(s_rgbBuffer.data(), CAMERA_WIDTH, CAMERA_HEIGHT, s_nv12Buffer.data(), CAMERA_PITCH); + s_mutex.unlock(); + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + std::this_thread::yield(); + } + } + void OpenStream() + { + const auto formatId = FindCorrectFormat(); + if (!formatId) + return; + const auto stream = Cap_openStream(s_ctx, *s_device, *formatId); + if (stream == -1) + return; + s_capturing = true; + s_stream = stream; + } + void CloseStream() + { + s_capturing = false; + if (s_stream) + { + Cap_closeStream(s_ctx, *s_stream); + s_stream = std::nullopt; + } + } + void ResetBuffers() + { + std::ranges::fill(s_rgbBuffer, 0); + constexpr auto pixCount = CAMERA_HEIGHT * CAMERA_PITCH; + std::ranges::fill_n(s_nv12Buffer.begin(), pixCount, 16); + std::ranges::fill_n(s_nv12Buffer.begin() + pixCount, (pixCount / 2), 128); + } + + void Init() + { + { + std::scoped_lock lock(s_mutex); + if (s_running) + return; + s_running = true; + s_ctx = Cap_createContext(); + Cap_setLogLevel(4); + Cap_installCustomLogFunction(CaptureLogFunction); + } + + s_captureThread = std::thread(&CaptureWorker); + + const auto uniqueId = GetConfig().camera_id.GetValue(); + if (!uniqueId.empty()) + { + const auto devices = EnumerateDevices(); + for (CapDeviceID deviceId = 0; deviceId < devices.size(); ++deviceId) + { + if (devices[deviceId].uniqueId == uniqueId) + { + s_device = deviceId; + return; + } + } + } + ResetBuffers(); + } + void Deinit() + { + CloseStream(); + Cap_releaseContext(s_ctx); + s_running = false; + s_captureThread.join(); + } + void FillNV12Buffer(uint8* nv12Buffer) + { + std::scoped_lock lock(s_mutex); + std::ranges::copy(s_nv12Buffer, nv12Buffer); + } + + void FillRGBBuffer(uint8* rgbBuffer) + { + std::scoped_lock lock(s_mutex); + std::ranges::copy(s_rgbBuffer, rgbBuffer); + } + void SetDevice(uint32 deviceNo) + { + std::scoped_lock lock(s_mutex); + CloseStream(); + if (deviceNo == DEVICE_NONE) + { + s_device = std::nullopt; + ResetBuffers(); + return; + } + s_device = deviceNo; + if (s_refCount != 0) + OpenStream(); + } + void Open() + { + std::scoped_lock lock(s_mutex); + if (s_device && s_refCount == 0) + { + OpenStream(); + } + s_refCount += 1; + } + void Close() + { + std::scoped_lock lock(s_mutex); + if (s_refCount == 0) + return; + s_refCount -= 1; + if (s_refCount != 0) + return; + CloseStream(); + } + std::vector EnumerateDevices() + { + std::scoped_lock lock(s_mutex); + std::vector infos; + const auto deviceCount = Cap_getDeviceCount(s_ctx); + cemuLog_log(LogType::InputAPI, "Available video capture devices:"); + for (CapDeviceID deviceNo = 0; deviceNo < deviceCount; ++deviceNo) + { + const auto uniqueId = Cap_getDeviceUniqueID(s_ctx, deviceNo); + const auto name = Cap_getDeviceName(s_ctx, deviceNo); + DeviceInfo info; + info.uniqueId = uniqueId; + + if (name) + info.name = fmt::format("{}: {}", deviceNo, name); + else + info.name = fmt::format("{}: Unknown", deviceNo); + infos.push_back(info); + cemuLog_log(LogType::InputAPI, "{}", info.name); + } + if (infos.empty()) + cemuLog_log(LogType::InputAPI, "No available video capture devices"); + return infos; + } + void SaveDevice() + { + std::scoped_lock lock(s_mutex); + auto& config = GetConfig(); + const auto cameraId = s_device ? Cap_getDeviceUniqueID(s_ctx, *s_device) : ""; + config.camera_id = cameraId; + } + + std::optional GetCurrentDevice() + { + return s_device; + } +} // namespace CameraManager \ No newline at end of file diff --git a/src/camera/CameraManager.h b/src/camera/CameraManager.h new file mode 100644 index 00000000..69ff88dc --- /dev/null +++ b/src/camera/CameraManager.h @@ -0,0 +1,27 @@ +#pragma once + +#pragma once +#include + +namespace CameraManager +{ + struct DeviceInfo + { + std::string uniqueId; + std::string name; + }; + constexpr static uint32 DEVICE_NONE = std::numeric_limits::max(); + + void Init(); + void Deinit(); + void Open(); + void Close(); + + void FillNV12Buffer(uint8* nv12Buffer); + void FillRGBBuffer(uint8* rgbBuffer); + + void SetDevice(uint32 deviceNo); + std::vector EnumerateDevices(); + void SaveDevice(); + std::optional GetCurrentDevice(); +} // namespace CameraManager \ No newline at end of file diff --git a/src/camera/Rgb2Nv12.cpp b/src/camera/Rgb2Nv12.cpp new file mode 100644 index 00000000..7b599309 --- /dev/null +++ b/src/camera/Rgb2Nv12.cpp @@ -0,0 +1,65 @@ +// Based on https://github.com/cohenrotem/Rgb2NV12 +#include "Rgb2Nv12.h" + +constexpr static glm::mat3x3 COEFFICIENT_MATRIX = +{ + +0.257f, -0.148f, 0.439f, + +0.504f, -0.291f, -0.368f, + +0.098f, +0.439f, -0.071f}; + +constexpr static glm::mat4x3 OFFSET_MATRIX = { + 16.0f + 0.5f, 128.0f + 2.0f, 128.0f + 2.0f, + 16.0f + 0.5f, 128.0f + 2.0f, 128.0f + 2.0f, + 16.0f + 0.5f, 128.0f + 2.0f, 128.0f + 2.0f, + 16.0f + 0.5f, 128.0f + 2.0f, 128.0f + 2.0f}; + +static void Rgb2Nv12TwoRows(const uint8* topLine, + const uint8* bottomLine, + unsigned imageWidth, + uint8* topLineY, + uint8* bottomLineY, + uint8* uv) +{ + auto* topIn = reinterpret_cast(topLine); + auto* botIn = reinterpret_cast(bottomLine); + + for (auto x = 0u; x < imageWidth; x += 2) + { + const glm::mat4x3 rgbMatrix{ + topIn[x], + topIn[x + 1], + botIn[x], + botIn[x + 1], + }; + + const auto result = COEFFICIENT_MATRIX * rgbMatrix + OFFSET_MATRIX; + + topLineY[x + 0] = result[0].s; + topLineY[x + 1] = result[1].s; + bottomLineY[x + 0] = result[2].s; + bottomLineY[x + 1] = result[3].s; + + uv[x + 0] = (result[0].t + result[1].t + result[2].t + result[3].t) * 0.25f; + uv[x + 1] = (result[0].p + result[1].p + result[2].p + result[3].p) * 0.25f; + } +} + +void Rgb2Nv12(const uint8* rgbImage, + unsigned imageWidth, + unsigned imageHeight, + uint8* outNv12Image, + unsigned nv12Pitch) +{ + cemu_assert_debug(!((imageWidth | imageHeight) & 1)); + unsigned char* UV = outNv12Image + nv12Pitch * imageHeight; + + for (auto row = 0u; row < imageHeight; row += 2) + { + Rgb2Nv12TwoRows(&rgbImage[row * imageWidth * 3], + &rgbImage[(row + 1) * imageWidth * 3], + imageWidth, + &outNv12Image[row * nv12Pitch], + &outNv12Image[(row + 1) * nv12Pitch], + &UV[(row / 2) * nv12Pitch]); + } +} \ No newline at end of file diff --git a/src/camera/Rgb2Nv12.h b/src/camera/Rgb2Nv12.h new file mode 100644 index 00000000..42178809 --- /dev/null +++ b/src/camera/Rgb2Nv12.h @@ -0,0 +1,9 @@ +#pragma once + +#pragma once + +void Rgb2Nv12(const uint8* rgbImage, + unsigned imageWidth, + unsigned imageHeight, + uint8* outNv12Image, + unsigned nv12Pitch); \ No newline at end of file diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 097587f8..83cd594f 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; } diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index 6778ceb5..5ce6fbba 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -503,6 +503,10 @@ struct CemuConfig // debug ConfigValueBounds crash_dump{ CrashDump::Disabled }; ConfigValue gdb_port{ 1337 }; + + // camera + ConfigValue camera_id; + #if 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 12fc62d9..c8ef44fa 100644 --- a/src/gui/wxgui/CMakeLists.txt +++ b/src/gui/wxgui/CMakeLists.txt @@ -114,6 +114,8 @@ add_library(CemuWxGui STATIC wxcomponents/checktree.h wxgui.h wxHelper.h + CameraSettingsWindow.cpp + CameraSettingsWindow.h ) if (ENABLE_METAL) diff --git a/src/gui/wxgui/CameraSettingsWindow.cpp b/src/gui/wxgui/CameraSettingsWindow.cpp new file mode 100644 index 00000000..24cd96db --- /dev/null +++ b/src/gui/wxgui/CameraSettingsWindow.cpp @@ -0,0 +1,97 @@ +#include "CameraSettingsWindow.h" + +#include "camera/CameraManager.h" + +#include +#include +#include +#include + +constexpr unsigned CAMERA_WIDTH = 640; +constexpr unsigned CAMERA_HEIGHT = 480; + +CameraSettingsWindow::CameraSettingsWindow(wxWindow* parent) + : wxDialog(parent, wxID_ANY, _("Camera settings"), wxDefaultPosition), + m_imageBitmap(CAMERA_WIDTH, CAMERA_HEIGHT, 24), m_imageBuffer(CAMERA_WIDTH * CAMERA_HEIGHT * 3) +{ + + CameraManager::Init(); + CameraManager::Open(); + auto* rootSizer = new wxBoxSizer(wxVERTICAL); + { + auto* topSizer = new wxBoxSizer(wxHORIZONTAL); + { + m_cameraChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, {300, -1}); + m_cameraChoice->Bind(wxEVT_CHOICE, &CameraSettingsWindow::OnSelectCameraChoice, this); + + m_refreshButton = new wxButton(this, wxID_ANY, wxString::FromUTF8("⟳")); + m_refreshButton->Fit(); + m_refreshButton->Bind(wxEVT_BUTTON, &CameraSettingsWindow::OnRefreshPressed, this); + wxQueueEvent(m_refreshButton, new wxCommandEvent{wxEVT_BUTTON}); + + topSizer->Add(m_cameraChoice); + topSizer->Add(m_refreshButton); + } + + m_imageWindow = new wxWindow(this, wxID_ANY, wxDefaultPosition, {CAMERA_WIDTH, CAMERA_HEIGHT}); + rootSizer->Add(topSizer); + rootSizer->Add(m_imageWindow, wxEXPAND); + } + SetSizerAndFit(rootSizer); + m_imageUpdateTimer.Bind(wxEVT_TIMER, &CameraSettingsWindow::UpdateImage, this); + m_imageUpdateTimer.Start(33, wxTIMER_CONTINUOUS); + this->Bind(wxEVT_CLOSE_WINDOW, &CameraSettingsWindow::OnClose, this); +} +void CameraSettingsWindow::OnSelectCameraChoice(wxCommandEvent&) +{ + const auto selection = m_cameraChoice->GetSelection(); + if (selection < 0) + return; + if (selection == 0) + CameraManager::SetDevice(CameraManager::DEVICE_NONE); + else + CameraManager::SetDevice(selection - 1); +} +void CameraSettingsWindow::OnRefreshPressed(wxCommandEvent&) +{ + wxArrayString choices = {_("None")}; + for (const auto& entry : CameraManager::EnumerateDevices()) + { + choices.push_back(entry.name); + } + m_cameraChoice->Set(choices); + if (auto currentDevice = CameraManager::GetCurrentDevice()) + m_cameraChoice->SetSelection(*currentDevice + 1); +} +void CameraSettingsWindow::UpdateImage(const wxTimerEvent&) +{ + CameraManager::FillRGBBuffer(m_imageBuffer.data()); + + wxNativePixelData data{m_imageBitmap}; + if (!data) + return; + wxNativePixelData::Iterator p{data}; + for (auto row = 0u; row < CAMERA_HEIGHT; ++row) + { + const auto* rowPtr = m_imageBuffer.data() + row * CAMERA_WIDTH * 3; + wxNativePixelData::Iterator rowStart = p; + for (auto col = 0u; col < 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); + } + + wxClientDC dc{m_imageWindow}; + dc.DrawBitmap(m_imageBitmap, 0, 0); +} +void CameraSettingsWindow::OnClose(wxCloseEvent& event) +{ + CameraManager::Close(); + CameraManager::SaveDevice(); + event.Skip(); +} \ No newline at end of file diff --git a/src/gui/wxgui/CameraSettingsWindow.h b/src/gui/wxgui/CameraSettingsWindow.h new file mode 100644 index 00000000..1a6dc6df --- /dev/null +++ b/src/gui/wxgui/CameraSettingsWindow.h @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include +#include + +class CameraSettingsWindow : public wxDialog +{ + wxChoice* m_cameraChoice; + wxButton* m_refreshButton; + wxWindow* m_imageWindow; + wxBitmap m_imageBitmap; + wxTimer m_imageUpdateTimer; + std::vector m_imageBuffer; + public: + explicit CameraSettingsWindow(wxWindow* parent); + void OnSelectCameraChoice(wxCommandEvent&); + void OnRefreshPressed(wxCommandEvent&); + void UpdateImage(const wxTimerEvent&); + void OnClose(wxCloseEvent& event); +}; \ No newline at end of file From 5c7f2b3fdc1ccca68f07cb90b6e1f05567c69b11 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Sun, 22 Feb 2026 00:22:37 +0000 Subject: [PATCH 02/28] Check buffer size --- src/Cafe/OS/libs/camera/camera.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Cafe/OS/libs/camera/camera.cpp b/src/Cafe/OS/libs/camera/camera.cpp index 70cb81bc..1ae98691 100644 --- a/src/Cafe/OS/libs/camera/camera.cpp +++ b/src/Cafe/OS/libs/camera/camera.cpp @@ -13,6 +13,7 @@ namespace camera { constexpr unsigned CAMERA_WIDTH = 640; constexpr unsigned CAMERA_HEIGHT = 480; + constexpr unsigned CAMERA_PITCH = 768; enum CAMStatus : sint32 { @@ -247,6 +248,7 @@ namespace camera return CAM_STATUS_INVALID_HANDLE; if (!targetSurface || targetSurface->data.IsNull() || targetSurface->size < 1) return CAM_STATUS_INVALID_ARG; + cemu_assert_debug(targetSurface->size >= ((CAMERA_HEIGHT * CAMERA_PITCH * 3) >> 1)); auto lock = std::scoped_lock(s_instance.mutex); if (!s_instance.initialized) return CAM_STATUS_UNINITIALIZED; From 1bc6ffbb9a986048ce1f6d9d04e1acf17fb52165 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Sun, 22 Feb 2026 00:23:28 +0000 Subject: [PATCH 03/28] Add Camera Settings menu option --- src/gui/wxgui/MainWindow.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/gui/wxgui/MainWindow.cpp b/src/gui/wxgui/MainWindow.cpp index 4ef4fe08..590974a9 100644 --- a/src/gui/wxgui/MainWindow.cpp +++ b/src/gui/wxgui/MainWindow.cpp @@ -28,6 +28,8 @@ #include "wxgui/CemuApp.h" #include "wxgui/CemuUpdateWindow.h" #include "wxgui/LoggingWindow.h" +#include "wxgui/CameraSettingsWindow.h" + #include "config/ActiveSettings.h" #include "config/LaunchSettings.h" @@ -102,6 +104,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, @@ -203,6 +206,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) @@ -946,6 +950,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 +2260,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")); From 906bb2b6cbded7c72867491f1cef63f90689e541 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Sun, 22 Feb 2026 00:23:52 +0000 Subject: [PATCH 04/28] Fix settings window preview --- src/gui/wxgui/CameraSettingsWindow.cpp | 14 +++++++++++--- src/gui/wxgui/CameraSettingsWindow.h | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/gui/wxgui/CameraSettingsWindow.cpp b/src/gui/wxgui/CameraSettingsWindow.cpp index 24cd96db..324bb74c 100644 --- a/src/gui/wxgui/CameraSettingsWindow.cpp +++ b/src/gui/wxgui/CameraSettingsWindow.cpp @@ -34,13 +34,17 @@ CameraSettingsWindow::CameraSettingsWindow(wxWindow* parent) } m_imageWindow = new wxWindow(this, wxID_ANY, wxDefaultPosition, {CAMERA_WIDTH, CAMERA_HEIGHT}); + m_imageWindow->SetBackgroundStyle(wxBG_STYLE_PAINT); + rootSizer->Add(topSizer); rootSizer->Add(m_imageWindow, wxEXPAND); } SetSizerAndFit(rootSizer); m_imageUpdateTimer.Bind(wxEVT_TIMER, &CameraSettingsWindow::UpdateImage, this); - m_imageUpdateTimer.Start(33, wxTIMER_CONTINUOUS); + 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&) { @@ -63,10 +67,10 @@ void CameraSettingsWindow::OnRefreshPressed(wxCommandEvent&) if (auto currentDevice = CameraManager::GetCurrentDevice()) m_cameraChoice->SetSelection(*currentDevice + 1); } + void CameraSettingsWindow::UpdateImage(const wxTimerEvent&) { CameraManager::FillRGBBuffer(m_imageBuffer.data()); - wxNativePixelData data{m_imageBitmap}; if (!data) return; @@ -85,8 +89,12 @@ void CameraSettingsWindow::UpdateImage(const wxTimerEvent&) p = rowStart; p.OffsetY(data, 1); } + m_imageWindow->Refresh(); +} - wxClientDC dc{m_imageWindow}; +void CameraSettingsWindow::DrawImage(const wxPaintEvent&) +{ + wxAutoBufferedPaintDC dc{m_imageWindow}; dc.DrawBitmap(m_imageBitmap, 0, 0); } void CameraSettingsWindow::OnClose(wxCloseEvent& event) diff --git a/src/gui/wxgui/CameraSettingsWindow.h b/src/gui/wxgui/CameraSettingsWindow.h index 1a6dc6df..6fc29358 100644 --- a/src/gui/wxgui/CameraSettingsWindow.h +++ b/src/gui/wxgui/CameraSettingsWindow.h @@ -17,5 +17,6 @@ class CameraSettingsWindow : public wxDialog void OnSelectCameraChoice(wxCommandEvent&); void OnRefreshPressed(wxCommandEvent&); void UpdateImage(const wxTimerEvent&); + void DrawImage(const wxPaintEvent&); void OnClose(wxCloseEvent& event); }; \ No newline at end of file From 990f75f7b6c3c4def62591f72efb51780d0c72ea Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Sun, 22 Feb 2026 00:48:40 +0000 Subject: [PATCH 05/28] Fix config saving --- src/camera/CameraManager.cpp | 6 +++--- src/config/CemuConfig.cpp | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/camera/CameraManager.cpp b/src/camera/CameraManager.cpp index 8fbcbea9..cda6dde4 100644 --- a/src/camera/CameraManager.cpp +++ b/src/camera/CameraManager.cpp @@ -216,9 +216,9 @@ namespace CameraManager void SaveDevice() { std::scoped_lock lock(s_mutex); - auto& config = GetConfig(); - const auto cameraId = s_device ? Cap_getDeviceUniqueID(s_ctx, *s_device) : ""; - config.camera_id = cameraId; + const std::string cameraId = s_device ? Cap_getDeviceUniqueID(s_ctx, *s_device) : ""; + GetConfig().camera_id.SetValue(cameraId); + GetConfigHandle().Save(); } std::optional GetCurrentDevice() diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 83cd594f..2504d030 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -292,7 +292,7 @@ XMLConfigParser CemuConfig::Load(XMLConfigParser& parser) emulated_usb_devices.emulate_dimensions_toypad = usbdevices.get("EmulateDimensionsToypad", emulated_usb_devices.emulate_dimensions_toypad); - auto camera = parser.get("camera"); + auto camera = parser.get("Camera"); camera_id = camera.get("DeviceId", ""); return parser; @@ -459,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; } From 9fff63928b80b04b6bbf0b1ff6715b878eb57266 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Sun, 22 Feb 2026 00:54:54 +0000 Subject: [PATCH 06/28] Add tooltip --- src/gui/wxgui/CameraSettingsWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/wxgui/CameraSettingsWindow.cpp b/src/gui/wxgui/CameraSettingsWindow.cpp index 324bb74c..17827268 100644 --- a/src/gui/wxgui/CameraSettingsWindow.cpp +++ b/src/gui/wxgui/CameraSettingsWindow.cpp @@ -3,7 +3,6 @@ #include "camera/CameraManager.h" #include -#include #include #include @@ -23,6 +22,7 @@ CameraSettingsWindow::CameraSettingsWindow(wxWindow* parent) { m_cameraChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, {300, -1}); m_cameraChoice->Bind(wxEVT_CHOICE, &CameraSettingsWindow::OnSelectCameraChoice, this); + m_cameraChoice->SetToolTip(_("Cameras are only listed if they support 640x480")); m_refreshButton = new wxButton(this, wxID_ANY, wxString::FromUTF8("⟳")); m_refreshButton->Fit(); From 6ea3a4d9a417b28992f1fe53bb283bad14975255 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Sun, 22 Feb 2026 02:30:32 +0000 Subject: [PATCH 07/28] Some cleanup --- src/Cafe/OS/libs/camera/camera.cpp | 545 +++++++++++++------------ src/camera/CameraManager.cpp | 415 ++++++++++--------- src/camera/CameraManager.h | 14 +- src/gui/wxgui/CameraSettingsWindow.cpp | 147 +++---- 4 files changed, 571 insertions(+), 550 deletions(-) diff --git a/src/Cafe/OS/libs/camera/camera.cpp b/src/Cafe/OS/libs/camera/camera.cpp index 1ae98691..eba6c022 100644 --- a/src/Cafe/OS/libs/camera/camera.cpp +++ b/src/Cafe/OS/libs/camera/camera.cpp @@ -11,307 +11,310 @@ namespace camera { - constexpr unsigned CAMERA_WIDTH = 640; - constexpr unsigned CAMERA_HEIGHT = 480; - constexpr unsigned CAMERA_PITCH = 768; + 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 + }; - 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 - }; + enum class CAMFps : uint32 + { + _15 = 0, + _30 = 1 + }; - enum class CAMFps : uint32 - { - _15 = 0, - _30 = 1 - }; + enum class CAMEventType : uint32 + { + Decode = 0, + Detached = 1 + }; - enum class CAMEventType : uint32 - { - Decode = 0, - Detached = 1 - }; + enum class CAMForceDisplay + { + None = 0, + DRC = 1 + }; - enum class CAMForceDisplay - { - None = 0, - DRC = 1 - }; + enum class CAMImageType : uint32 + { + Default = 0 + }; - enum class CAMImageType : uint32 - { - Default = 0 - }; + struct CAMImageInfo + { + betype type; + uint32be height; + uint32be width; + }; - struct CAMImageInfo - { - betype type; - uint32be height; - uint32be width; - }; - static_assert(sizeof(CAMImageInfo) == 0x0C); + 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]; - }; - static_assert(sizeof(CAMInitInfo_t) == 0x34); + struct CAMInitInfo_t + { + CAMImageInfo imageInfo; + uint32be workMemorySize; + MEMPTR workMemoryData; + MEMPTR callback; + betype forceDisplay; + betype fps; + uint32be threadFlags; + uint8 unk[0x10]; + }; - struct CAMTargetSurface - { - sint32be size; - MEMPTR data; - uint8 unused[0x18]; - }; - static_assert(sizeof(CAMTargetSurface) == 0x20); + static_assert(sizeof(CAMInitInfo_t) == 0x34); - struct CAMDecodeEventParam - { - betype type; - MEMPTR data; - uint32be channel; - uint32be errored; - }; - static_assert(sizeof(CAMDecodeEventParam) == 0x10); + struct CAMTargetSurface + { + sint32be size; + MEMPTR data; + uint8 unused[0x18]; + }; - constexpr static int32_t CAM_HANDLE = 0; + static_assert(sizeof(CAMTargetSurface) == 0x20); - struct - { - std::recursive_mutex mutex{}; - bool initialized = false; - bool shouldTriggerCallback = false; - std::atomic_bool isOpen = false; - std::atomic_bool isExiting = false; - bool isWorking = false; - unsigned fps = 30; - MEMPTR eventCallback = nullptr; - RingBuffer, 20> inTargetBuffers{}; - RingBuffer, 20> outTargetBuffers{}; - } s_instance; + struct CAMDecodeEventParam + { + betype type; + MEMPTR data; + uint32be channel; + uint32be errored; + }; - SysAllocator s_cameraEventData; - SysAllocator s_cameraWorkerThread; - SysAllocator s_cameraWorkerThreadStack; - SysAllocator> s_cameraWorkerThreadNameBuffer; - SysAllocator s_cameraOpenEvent; + static_assert(sizeof(CAMDecodeEventParam) == 0x10); - void WorkerThread(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()); + constexpr static int32_t CAM_HANDLE = 0; - while (!s_instance.isExiting) - { - coreinit::OSWaitEvent(s_cameraOpenEvent); - while (true) - { - if (!s_instance.isOpen || s_instance.isExiting) - { - // Fill leftover buffers before stopping - if (!s_instance.inTargetBuffers.HasData()) - break; - } - s_cameraEventData->type = CAMEventType::Decode; - s_cameraEventData->channel = 0; + struct + { + std::recursive_mutex mutex{}; + bool initialized = false; + bool shouldTriggerCallback = false; + std::atomic_bool isOpen = false; + std::atomic_bool isExiting = false; + bool isWorking = false; + unsigned fps = 30; + MEMPTR eventCallback = nullptr; + RingBuffer, 20> inTargetBuffers{}; + RingBuffer, 20> outTargetBuffers{}; + } s_instance; - const auto surfaceBuffer = s_instance.inTargetBuffers.Pop(); - if (surfaceBuffer.IsNull()) - { - s_cameraEventData->data = nullptr; - s_cameraEventData->errored = true; - } - else - { - CameraManager::FillNV12Buffer(surfaceBuffer.GetPtr()); - s_cameraEventData->data = surfaceBuffer; - s_cameraEventData->errored = false; - } - PPCCoreCallback(s_instance.eventCallback, s_cameraEventData.GetMPTR()); - coreinit::OSSleepTicks(Espresso::TIMER_CLOCK / (s_instance.fps - 1)); - } - } - coreinit::OSExitThread(0); - } + SysAllocator s_cameraEventData; + SysAllocator s_cameraWorkerThread; + SysAllocator s_cameraWorkerThreadStack; + SysAllocator> s_cameraWorkerThreadNameBuffer; + SysAllocator s_cameraOpenEvent; - sint32 CAMGetMemReq(const CAMImageInfo* info) - { - if (!info) - return CAM_STATUS_INVALID_ARG; - return 1 * 1024; // always return 1KB - } + void WorkerThread(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()); - CAMStatus CAMCheckMemSegmentation(void* startAddr, uint32 size) - { - if (!startAddr || size == 0) - return CAM_STATUS_INVALID_ARG; - return CAM_STATUS_SUCCESS; - } + while (!s_instance.isExiting) + { + coreinit::OSWaitEvent(s_cameraOpenEvent); + while (true) + { + if (!s_instance.isOpen || s_instance.isExiting) + { + // Fill leftover buffers before stopping + if (!s_instance.inTargetBuffers.HasData()) + break; + } + s_cameraEventData->type = CAMEventType::Decode; + s_cameraEventData->channel = 0; - sint32 CAMInit(uint32 cameraId, const CAMInitInfo_t* initInfo, betype* error) - { - *error = CAM_STATUS_SUCCESS; - std::scoped_lock lock(s_instance.mutex); - if (s_instance.initialized) - { - *error = CAM_STATUS_DEVICE_IN_USE; - return -1; - } + const auto surfaceBuffer = s_instance.inTargetBuffers.Pop(); + if (surfaceBuffer.IsNull()) + { + 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; + } + PPCCoreCallback(s_instance.eventCallback, s_cameraEventData.GetMPTR()); + coreinit::OSSleepTicks(Espresso::TIMER_CLOCK / (s_instance.fps - 1)); + } + } + coreinit::OSExitThread(0); + } - if (!initInfo || !initInfo->workMemoryData || - !match_any_of(initInfo->forceDisplay, CAMForceDisplay::None, CAMForceDisplay::DRC) || - !match_any_of(initInfo->fps, CAMFps::_15, CAMFps::_30) || - initInfo->imageInfo.type != CAMImageType::Default) - { - *error = CAM_STATUS_INVALID_ARG; - return -1; - } - CameraManager::Init(); + sint32 CAMGetMemReq(const CAMImageInfo* info) + { + if (!info) + return CAM_STATUS_INVALID_ARG; + return 1 * 1024; // always return 1KB + } - cemu_assert_debug(initInfo->forceDisplay != CAMForceDisplay::DRC); - cemu_assert_debug(initInfo->workMemorySize != 0); - cemu_assert_debug(initInfo->imageInfo.type == CAMImageType::Default); + CAMStatus CAMCheckMemSegmentation(void* startAddr, uint32 size) + { + if (!startAddr || size == 0) + return CAM_STATUS_INVALID_ARG; + return CAM_STATUS_SUCCESS; + } - s_instance.isExiting = false; - s_instance.fps = initInfo->fps == CAMFps::_15 ? 15 : 30; - s_instance.initialized = true; - s_instance.eventCallback = initInfo->callback; + sint32 CAMInit(uint32 cameraId, const CAMInitInfo_t* initInfo, betype* error) + { + *error = CAM_STATUS_SUCCESS; + std::scoped_lock lock(s_instance.mutex); + if (s_instance.initialized) + { + *error = CAM_STATUS_DEVICE_IN_USE; + return -1; + } - coreinit::OSInitEvent(s_cameraOpenEvent, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, coreinit::OSEvent::EVENT_MODE::MODE_AUTO); + if (!initInfo || !initInfo->workMemoryData || + !match_any_of(initInfo->forceDisplay, CAMForceDisplay::None, CAMForceDisplay::DRC) || + !match_any_of(initInfo->fps, CAMFps::_15, CAMFps::_30) || + initInfo->imageInfo.type != CAMImageType::Default) + { + *error = CAM_STATUS_INVALID_ARG; + return -1; + } + CameraManager::Init(); - coreinit::__OSCreateThreadType( - s_cameraWorkerThread, RPLLoader_MakePPCCallable(WorkerThread), 0, nullptr, - s_cameraWorkerThreadStack.GetPtr() + s_cameraWorkerThreadStack.GetByteSize(), s_cameraWorkerThreadStack.GetByteSize(), - 0x10, initInfo->threadFlags & 7, OSThread_t::THREAD_TYPE::TYPE_DRIVER); - s_cameraWorkerThreadNameBuffer->assign("CameraWorkerThread"); - coreinit::OSSetThreadName(s_cameraWorkerThread.GetPtr(), s_cameraWorkerThreadNameBuffer->c_str()); - coreinit::OSResumeThread(s_cameraWorkerThread.GetPtr()); - return CAM_STATUS_SUCCESS; - } + cemu_assert_debug(initInfo->forceDisplay != CAMForceDisplay::DRC); + cemu_assert_debug(initInfo->workMemorySize != 0); + cemu_assert_debug(initInfo->imageInfo.type == CAMImageType::Default); - CAMStatus CAMClose(sint32 camHandle) - { - if (camHandle != CAM_HANDLE) - return CAM_STATUS_INVALID_HANDLE; - { - std::scoped_lock lock(s_instance.mutex); - if (!s_instance.initialized || !s_instance.isOpen) - return CAM_STATUS_UNINITIALIZED; - s_instance.isOpen = false; - } - CameraManager::Close(); - return CAM_STATUS_SUCCESS; - } + s_instance.isExiting = false; + s_instance.fps = initInfo->fps == CAMFps::_15 ? 15 : 30; + s_instance.initialized = true; + s_instance.eventCallback = initInfo->callback; - CAMStatus CAMOpen(sint32 camHandle) - { - if (camHandle != CAM_HANDLE) - return CAM_STATUS_INVALID_HANDLE; - auto lock = std::scoped_lock(s_instance.mutex); - 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(); - s_instance.outTargetBuffers.Clear(); - return CAM_STATUS_SUCCESS; - } + coreinit::OSInitEvent(s_cameraOpenEvent, coreinit::OSEvent::EVENT_STATE::STATE_NOT_SIGNALED, + coreinit::OSEvent::EVENT_MODE::MODE_AUTO); - 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 >= ((CAMERA_HEIGHT * CAMERA_PITCH * 3) >> 1)); - auto lock = std::scoped_lock(s_instance.mutex); - 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; - } + coreinit::__OSCreateThreadType( + s_cameraWorkerThread, RPLLoader_MakePPCCallable(WorkerThread), 0, nullptr, + s_cameraWorkerThreadStack.GetPtr() + s_cameraWorkerThreadStack.GetByteSize(), + s_cameraWorkerThreadStack.GetByteSize(), + 0x10, initInfo->threadFlags & 7, OSThread_t::THREAD_TYPE::TYPE_DRIVER); + s_cameraWorkerThreadNameBuffer->assign("CameraWorkerThread"); + coreinit::OSSetThreadName(s_cameraWorkerThread.GetPtr(), s_cameraWorkerThreadNameBuffer->c_str()); + coreinit::OSResumeThread(s_cameraWorkerThread.GetPtr()); + return CAM_STATUS_SUCCESS; + } - void CAMExit(sint32 camHandle) - { - if (camHandle != CAM_HANDLE) - return; - std::scoped_lock lock(s_instance.mutex); - if (!s_instance.initialized) - return; - s_instance.isExiting = true; - if (s_instance.isOpen) - CAMClose(camHandle); - coreinit::OSSignalEvent(s_cameraOpenEvent.GetPtr()); - coreinit::OSJoinThread(s_cameraWorkerThread, nullptr); - s_instance.initialized = false; - } + CAMStatus CAMClose(sint32 camHandle) + { + if (camHandle != CAM_HANDLE) + return CAM_STATUS_INVALID_HANDLE; + { + std::scoped_lock lock(s_instance.mutex); + if (!s_instance.initialized || !s_instance.isOpen) + return CAM_STATUS_UNINITIALIZED; + s_instance.isOpen = false; + } + CameraManager::Close(); + return CAM_STATUS_SUCCESS; + } - void reset() - { - CAMExit(0); - } + CAMStatus CAMOpen(sint32 camHandle) + { + if (camHandle != CAM_HANDLE) + return CAM_STATUS_INVALID_HANDLE; + auto lock = std::scoped_lock(s_instance.mutex); + 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(); + s_instance.outTargetBuffers.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); + auto lock = std::scoped_lock(s_instance.mutex); + 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) + return; + std::scoped_lock lock(s_instance.mutex); + if (!s_instance.initialized) + return; + s_instance.isExiting = true; + if (s_instance.isOpen) + CAMClose(camHandle); + coreinit::OSSignalEvent(s_cameraOpenEvent.GetPtr()); + coreinit::OSJoinThread(s_cameraWorkerThread, nullptr); + 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; + void reset() + { + CAMExit(0); + } - COSModule* GetModule() - { - return &s_COScameraModule; - } + class : public COSModule + { + public: + std::string_view GetName() override + { + return "camera"; + } + + 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 rpl_entry(uint32 moduleHandle, coreinit::RplEntryReason reason) override + { + if (reason == coreinit::RplEntryReason::Loaded) + { + reset(); + } + else if (reason == coreinit::RplEntryReason::Unloaded) + { + // todo + } + } + } s_COScameraModule; + + COSModule* GetModule() + { + return &s_COScameraModule; + } } - diff --git a/src/camera/CameraManager.cpp b/src/camera/CameraManager.cpp index cda6dde4..6a17efce 100644 --- a/src/camera/CameraManager.cpp +++ b/src/camera/CameraManager.cpp @@ -11,218 +11,227 @@ #include -constexpr unsigned CAMERA_WIDTH = 640; -constexpr unsigned CAMERA_HEIGHT = 480; -constexpr unsigned CAMERA_PITCH = 768; - namespace CameraManager { - std::mutex s_mutex; - CapContext s_ctx; - std::optional s_device; - std::optional s_stream; - std::array s_rgbBuffer; - std::array s_nv12Buffer; - int s_refCount = 0; - std::thread s_captureThread; - std::atomic_bool s_capturing = false; - std::atomic_bool s_running = false; + std::mutex s_mutex; + CapContext s_ctx; + std::optional s_device; + std::optional s_stream; + std::array s_rgbBuffer; + std::array s_nv12Buffer; + int s_refCount = 0; + std::thread s_captureThread; + std::atomic_bool s_capturing = false; + std::atomic_bool s_running = false; - std::string FourCC(uint32le value) - { - return { - static_cast((value >> 0) & 0xFF), - static_cast((value >> 8) & 0xFF), - static_cast((value >> 16) & 0xFF), - static_cast((value >> 24) & 0xFF)}; - } + std::string FourCC(uint32le value) + { + return { + static_cast((value >> 0) & 0xFF), + static_cast((value >> 8) & 0xFF), + static_cast((value >> 16) & 0xFF), + static_cast((value >> 24) & 0xFF) + }; + } - void CaptureLogFunction(uint32_t level, const char* string) - { - cemuLog_log(LogType::InputAPI, "OpenPNPCapture: {}: {}", level, string); - } + void CaptureLogFunction(uint32_t level, const char* string) + { + cemuLog_log(LogType::InputAPI, "OpenPNPCapture: {}: {}", level, string); + } - std::optional FindCorrectFormat() - { - const auto device = *s_device; - cemuLog_log(LogType::InputAPI, "Video capture device '{}' available formats:", Cap_getDeviceName(s_ctx, device)); - const auto formatCount = Cap_getNumFormats(s_ctx, device); - for (int32_t formatId = 0; formatId < formatCount; ++formatId) - { - CapFormatInfo formatInfo; - if (Cap_getFormatInfo(s_ctx, device, formatId, &formatInfo) != CAPRESULT_OK) - continue; - cemuLog_log(LogType::Force, "{}: {} {}x{} @ {} fps, {} bpp", formatId, FourCC(formatInfo.fourcc), formatInfo.width, formatInfo.height, formatInfo.fps, formatInfo.bpp); - if (formatInfo.width == CAMERA_WIDTH && formatInfo.height == CAMERA_HEIGHT) - { - cemuLog_log(LogType::Force, "Selected video format {}", formatId); - return formatId; - } - } - cemuLog_log(LogType::Force, "Failed to find suitable video format"); - return std::nullopt; - } + std::optional FindCorrectFormat() + { + const auto device = *s_device; + cemuLog_log(LogType::InputAPI, "Video capture device '{}' available formats:", + Cap_getDeviceName(s_ctx, device)); + const auto formatCount = Cap_getNumFormats(s_ctx, device); + for (int32_t formatId = 0; formatId < formatCount; ++formatId) + { + CapFormatInfo formatInfo; + if (Cap_getFormatInfo(s_ctx, device, formatId, &formatInfo) != CAPRESULT_OK) + continue; + cemuLog_log(LogType::Force, "{}: {} {}x{} @ {} fps, {} bpp", formatId, FourCC(formatInfo.fourcc), + formatInfo.width, formatInfo.height, formatInfo.fps, formatInfo.bpp); + if (formatInfo.width == CAMERA_WIDTH && formatInfo.height == CAMERA_HEIGHT) + { + cemuLog_log(LogType::Force, "Selected video format {}", formatId); + return formatId; + } + } + cemuLog_log(LogType::Force, "Failed to find suitable video format"); + return std::nullopt; + } - void CaptureWorker() - { - SetThreadName("CameraManager"); - while (s_running) - { - while (s_capturing) - { - s_mutex.lock(); - if (s_stream && Cap_hasNewFrame(s_ctx, *s_stream) && - Cap_captureFrame(s_ctx, *s_stream, s_rgbBuffer.data(), s_rgbBuffer.size()) == CAPRESULT_OK) - Rgb2Nv12(s_rgbBuffer.data(), CAMERA_WIDTH, CAMERA_HEIGHT, s_nv12Buffer.data(), CAMERA_PITCH); - s_mutex.unlock(); - std::this_thread::sleep_for(std::chrono::milliseconds(30)); - } - std::this_thread::sleep_for(std::chrono::seconds(1)); - std::this_thread::yield(); - } - } - void OpenStream() - { - const auto formatId = FindCorrectFormat(); - if (!formatId) - return; - const auto stream = Cap_openStream(s_ctx, *s_device, *formatId); - if (stream == -1) - return; - s_capturing = true; - s_stream = stream; - } - void CloseStream() - { - s_capturing = false; - if (s_stream) - { - Cap_closeStream(s_ctx, *s_stream); - s_stream = std::nullopt; - } - } - void ResetBuffers() - { - std::ranges::fill(s_rgbBuffer, 0); - constexpr auto pixCount = CAMERA_HEIGHT * CAMERA_PITCH; - std::ranges::fill_n(s_nv12Buffer.begin(), pixCount, 16); - std::ranges::fill_n(s_nv12Buffer.begin() + pixCount, (pixCount / 2), 128); - } + void CaptureWorker() + { + SetThreadName("CameraManager"); + while (s_running) + { + while (s_capturing) + { + s_mutex.lock(); + if (s_stream && Cap_hasNewFrame(s_ctx, *s_stream) && + Cap_captureFrame(s_ctx, *s_stream, s_rgbBuffer.data(), s_rgbBuffer.size()) == CAPRESULT_OK) + Rgb2Nv12(s_rgbBuffer.data(), CAMERA_WIDTH, CAMERA_HEIGHT, s_nv12Buffer.data(), CAMERA_PITCH); + s_mutex.unlock(); + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + } + std::this_thread::sleep_for(std::chrono::seconds(1)); + std::this_thread::yield(); + } + } - void Init() - { - { - std::scoped_lock lock(s_mutex); - if (s_running) - return; - s_running = true; - s_ctx = Cap_createContext(); - Cap_setLogLevel(4); - Cap_installCustomLogFunction(CaptureLogFunction); - } + void OpenStream() + { + const auto formatId = FindCorrectFormat(); + if (!formatId) + return; + const auto stream = Cap_openStream(s_ctx, *s_device, *formatId); + if (stream == -1) + return; + s_capturing = true; + s_stream = stream; + } - s_captureThread = std::thread(&CaptureWorker); + void CloseStream() + { + s_capturing = false; + if (s_stream) + { + Cap_closeStream(s_ctx, *s_stream); + s_stream = std::nullopt; + } + } - const auto uniqueId = GetConfig().camera_id.GetValue(); - if (!uniqueId.empty()) - { - const auto devices = EnumerateDevices(); - for (CapDeviceID deviceId = 0; deviceId < devices.size(); ++deviceId) - { - if (devices[deviceId].uniqueId == uniqueId) - { - s_device = deviceId; - return; - } - } - } - ResetBuffers(); - } - void Deinit() - { - CloseStream(); - Cap_releaseContext(s_ctx); - s_running = false; - s_captureThread.join(); - } - void FillNV12Buffer(uint8* nv12Buffer) - { - std::scoped_lock lock(s_mutex); - std::ranges::copy(s_nv12Buffer, nv12Buffer); - } + void ResetBuffers() + { + std::ranges::fill(s_rgbBuffer, 0); + constexpr auto pixCount = CAMERA_HEIGHT * CAMERA_PITCH; + std::ranges::fill_n(s_nv12Buffer.begin(), pixCount, 16); + std::ranges::fill_n(s_nv12Buffer.begin() + pixCount, (pixCount / 2), 128); + } - void FillRGBBuffer(uint8* rgbBuffer) - { - std::scoped_lock lock(s_mutex); - std::ranges::copy(s_rgbBuffer, rgbBuffer); - } - void SetDevice(uint32 deviceNo) - { - std::scoped_lock lock(s_mutex); - CloseStream(); - if (deviceNo == DEVICE_NONE) - { - s_device = std::nullopt; - ResetBuffers(); - return; - } - s_device = deviceNo; - if (s_refCount != 0) - OpenStream(); - } - void Open() - { - std::scoped_lock lock(s_mutex); - if (s_device && s_refCount == 0) - { - OpenStream(); - } - s_refCount += 1; - } - void Close() - { - std::scoped_lock lock(s_mutex); - if (s_refCount == 0) - return; - s_refCount -= 1; - if (s_refCount != 0) - return; - CloseStream(); - } - std::vector EnumerateDevices() - { - std::scoped_lock lock(s_mutex); - std::vector infos; - const auto deviceCount = Cap_getDeviceCount(s_ctx); - cemuLog_log(LogType::InputAPI, "Available video capture devices:"); - for (CapDeviceID deviceNo = 0; deviceNo < deviceCount; ++deviceNo) - { - const auto uniqueId = Cap_getDeviceUniqueID(s_ctx, deviceNo); - const auto name = Cap_getDeviceName(s_ctx, deviceNo); - DeviceInfo info; - info.uniqueId = uniqueId; + void Init() + { + { + std::scoped_lock lock(s_mutex); + if (s_running) + return; + s_running = true; + s_ctx = Cap_createContext(); + Cap_setLogLevel(4); + Cap_installCustomLogFunction(CaptureLogFunction); + } - if (name) - info.name = fmt::format("{}: {}", deviceNo, name); - else - info.name = fmt::format("{}: Unknown", deviceNo); - infos.push_back(info); - cemuLog_log(LogType::InputAPI, "{}", info.name); - } - if (infos.empty()) - cemuLog_log(LogType::InputAPI, "No available video capture devices"); - return infos; - } - void SaveDevice() - { - std::scoped_lock lock(s_mutex); - const std::string cameraId = s_device ? Cap_getDeviceUniqueID(s_ctx, *s_device) : ""; - GetConfig().camera_id.SetValue(cameraId); - GetConfigHandle().Save(); - } + s_captureThread = std::thread(&CaptureWorker); - std::optional GetCurrentDevice() - { - return s_device; - } -} // namespace CameraManager \ No newline at end of file + const auto uniqueId = GetConfig().camera_id.GetValue(); + if (!uniqueId.empty()) + { + const auto devices = EnumerateDevices(); + for (CapDeviceID deviceId = 0; deviceId < devices.size(); ++deviceId) + { + if (devices[deviceId].uniqueId == uniqueId) + { + s_device = deviceId; + return; + } + } + } + ResetBuffers(); + } + + void Deinit() + { + CloseStream(); + Cap_releaseContext(s_ctx); + s_running = false; + s_captureThread.join(); + } + + void FillNV12Buffer(std::span nv12Buffer) + { + std::scoped_lock lock(s_mutex); + std::ranges::copy(s_nv12Buffer, nv12Buffer.data()); + } + + void FillRGBBuffer(std::span rgbBuffer) + { + std::scoped_lock lock(s_mutex); + std::ranges::copy(s_rgbBuffer, rgbBuffer.data()); + } + + void SetDevice(uint32 deviceNo) + { + std::scoped_lock lock(s_mutex); + CloseStream(); + if (deviceNo == DEVICE_NONE) + { + s_device = std::nullopt; + ResetBuffers(); + return; + } + s_device = deviceNo; + if (s_refCount != 0) + OpenStream(); + } + + void Open() + { + std::scoped_lock lock(s_mutex); + if (s_device && s_refCount == 0) + { + OpenStream(); + } + s_refCount += 1; + } + + void Close() + { + std::scoped_lock lock(s_mutex); + if (s_refCount == 0) + return; + s_refCount -= 1; + if (s_refCount != 0) + return; + CloseStream(); + } + + std::vector EnumerateDevices() + { + std::scoped_lock lock(s_mutex); + std::vector infos; + const auto deviceCount = Cap_getDeviceCount(s_ctx); + cemuLog_log(LogType::InputAPI, "Available video capture devices:"); + for (CapDeviceID deviceNo = 0; deviceNo < deviceCount; ++deviceNo) + { + const auto uniqueId = Cap_getDeviceUniqueID(s_ctx, deviceNo); + const auto name = Cap_getDeviceName(s_ctx, deviceNo); + DeviceInfo info; + info.uniqueId = uniqueId; + + if (name) + info.name = fmt::format("{}: {}", deviceNo, name); + else + info.name = fmt::format("{}: Unknown", deviceNo); + infos.push_back(info); + cemuLog_log(LogType::InputAPI, "{}", info.name); + } + if (infos.empty()) + cemuLog_log(LogType::InputAPI, "No available video capture devices"); + return infos; + } + + void SaveDevice() + { + std::scoped_lock lock(s_mutex); + const std::string cameraId = s_device ? Cap_getDeviceUniqueID(s_ctx, *s_device) : ""; + GetConfig().camera_id.SetValue(cameraId); + GetConfigHandle().Save(); + } + + std::optional GetCurrentDevice() + { + return s_device; + } +} // namespace CameraManager diff --git a/src/camera/CameraManager.h b/src/camera/CameraManager.h index 69ff88dc..583fdbf1 100644 --- a/src/camera/CameraManager.h +++ b/src/camera/CameraManager.h @@ -5,11 +5,19 @@ namespace CameraManager { + 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; }; + constexpr static uint32 DEVICE_NONE = std::numeric_limits::max(); void Init(); @@ -17,11 +25,11 @@ namespace CameraManager void Open(); void Close(); - void FillNV12Buffer(uint8* nv12Buffer); - void FillRGBBuffer(uint8* rgbBuffer); + void FillNV12Buffer(std::span nv12Buffer); + void FillRGBBuffer(std::span rgbBuffer); void SetDevice(uint32 deviceNo); std::vector EnumerateDevices(); void SaveDevice(); std::optional GetCurrentDevice(); -} // namespace CameraManager \ No newline at end of file +} // namespace CameraManager diff --git a/src/gui/wxgui/CameraSettingsWindow.cpp b/src/gui/wxgui/CameraSettingsWindow.cpp index 17827268..a72cca23 100644 --- a/src/gui/wxgui/CameraSettingsWindow.cpp +++ b/src/gui/wxgui/CameraSettingsWindow.cpp @@ -6,100 +6,101 @@ #include #include -constexpr unsigned CAMERA_WIDTH = 640; -constexpr unsigned CAMERA_HEIGHT = 480; - CameraSettingsWindow::CameraSettingsWindow(wxWindow* parent) - : wxDialog(parent, wxID_ANY, _("Camera settings"), wxDefaultPosition), - m_imageBitmap(CAMERA_WIDTH, CAMERA_HEIGHT, 24), m_imageBuffer(CAMERA_WIDTH * CAMERA_HEIGHT * 3) + : wxDialog(parent, wxID_ANY, _("Camera settings"), wxDefaultPosition), + m_imageBitmap(CameraManager::CAMERA_WIDTH, CameraManager::CAMERA_HEIGHT, 24), + m_imageBuffer(CameraManager::CAMERA_RGB_BUFFER_SIZE) { + CameraManager::Init(); + CameraManager::Open(); + auto* rootSizer = new wxBoxSizer(wxVERTICAL); + { + auto* topSizer = new wxBoxSizer(wxHORIZONTAL); + { + m_cameraChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, {300, -1}); + m_cameraChoice->Bind(wxEVT_CHOICE, &CameraSettingsWindow::OnSelectCameraChoice, this); + m_cameraChoice->SetToolTip(_("Cameras are only listed if they support 640x480")); - CameraManager::Init(); - CameraManager::Open(); - auto* rootSizer = new wxBoxSizer(wxVERTICAL); - { - auto* topSizer = new wxBoxSizer(wxHORIZONTAL); - { - m_cameraChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, {300, -1}); - m_cameraChoice->Bind(wxEVT_CHOICE, &CameraSettingsWindow::OnSelectCameraChoice, this); - m_cameraChoice->SetToolTip(_("Cameras are only listed if they support 640x480")); + m_refreshButton = new wxButton(this, wxID_ANY, wxString::FromUTF8("⟳")); + m_refreshButton->Fit(); + m_refreshButton->Bind(wxEVT_BUTTON, &CameraSettingsWindow::OnRefreshPressed, this); + wxQueueEvent(m_refreshButton, new wxCommandEvent{wxEVT_BUTTON}); - m_refreshButton = new wxButton(this, wxID_ANY, wxString::FromUTF8("⟳")); - m_refreshButton->Fit(); - m_refreshButton->Bind(wxEVT_BUTTON, &CameraSettingsWindow::OnRefreshPressed, this); - wxQueueEvent(m_refreshButton, new wxCommandEvent{wxEVT_BUTTON}); + topSizer->Add(m_cameraChoice); + topSizer->Add(m_refreshButton); + } - topSizer->Add(m_cameraChoice); - 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_imageWindow = new wxWindow(this, wxID_ANY, wxDefaultPosition, {CAMERA_WIDTH, CAMERA_HEIGHT}); - m_imageWindow->SetBackgroundStyle(wxBG_STYLE_PAINT); + rootSizer->Add(topSizer); + rootSizer->Add(m_imageWindow, wxEXPAND); + } + SetSizerAndFit(rootSizer); + m_imageUpdateTimer.Bind(wxEVT_TIMER, &CameraSettingsWindow::UpdateImage, this); + m_imageWindow->Bind(wxEVT_PAINT, &CameraSettingsWindow::DrawImage, this); + this->Bind(wxEVT_CLOSE_WINDOW, &CameraSettingsWindow::OnClose, this); - rootSizer->Add(topSizer); - rootSizer->Add(m_imageWindow, wxEXPAND); - } - SetSizerAndFit(rootSizer); - m_imageUpdateTimer.Bind(wxEVT_TIMER, &CameraSettingsWindow::UpdateImage, this); - m_imageWindow->Bind(wxEVT_PAINT, &CameraSettingsWindow::DrawImage, this); - this->Bind(wxEVT_CLOSE_WINDOW, &CameraSettingsWindow::OnClose, this); - - m_imageUpdateTimer.Start(33, wxTIMER_CONTINUOUS); + m_imageUpdateTimer.Start(33, wxTIMER_CONTINUOUS); } + void CameraSettingsWindow::OnSelectCameraChoice(wxCommandEvent&) { - const auto selection = m_cameraChoice->GetSelection(); - if (selection < 0) - return; - if (selection == 0) - CameraManager::SetDevice(CameraManager::DEVICE_NONE); - else - CameraManager::SetDevice(selection - 1); + const auto selection = m_cameraChoice->GetSelection(); + if (selection < 0) + return; + if (selection == 0) + CameraManager::SetDevice(CameraManager::DEVICE_NONE); + else + CameraManager::SetDevice(selection - 1); } + void CameraSettingsWindow::OnRefreshPressed(wxCommandEvent&) { - wxArrayString choices = {_("None")}; - for (const auto& entry : CameraManager::EnumerateDevices()) - { - choices.push_back(entry.name); - } - m_cameraChoice->Set(choices); - if (auto currentDevice = CameraManager::GetCurrentDevice()) - m_cameraChoice->SetSelection(*currentDevice + 1); + wxArrayString choices = {_("None")}; + for (const auto& entry : CameraManager::EnumerateDevices()) + { + choices.push_back(entry.name); + } + m_cameraChoice->Set(choices); + if (auto currentDevice = CameraManager::GetCurrentDevice()) + m_cameraChoice->SetSelection(*currentDevice + 1); } void CameraSettingsWindow::UpdateImage(const wxTimerEvent&) { - CameraManager::FillRGBBuffer(m_imageBuffer.data()); - wxNativePixelData data{m_imageBitmap}; - if (!data) - return; - wxNativePixelData::Iterator p{data}; - for (auto row = 0u; row < CAMERA_HEIGHT; ++row) - { - const auto* rowPtr = m_imageBuffer.data() + row * CAMERA_WIDTH * 3; - wxNativePixelData::Iterator rowStart = p; - for (auto col = 0u; col < 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(); + 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); + wxAutoBufferedPaintDC dc{m_imageWindow}; + dc.DrawBitmap(m_imageBitmap, 0, 0); } + void CameraSettingsWindow::OnClose(wxCloseEvent& event) { - CameraManager::Close(); - CameraManager::SaveDevice(); - event.Skip(); -} \ No newline at end of file + CameraManager::Close(); + CameraManager::SaveDevice(); + event.Skip(); +} From 2ffe6509d98989e48caaef118114ecd03689812d Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Sun, 22 Feb 2026 06:13:43 +0000 Subject: [PATCH 08/28] Update `openpnp-capture` and link statically --- CMakeLists.txt | 6 ++++-- dependencies/openpnp-capture | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 43e27dc3..46866ac2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -251,6 +251,8 @@ if (NOT ZArchive_FOUND) add_subdirectory("dependencies/ZArchive" EXCLUDE_FROM_ALL) endif() -add_subdirectory("dependencies/openpnp-capture" EXCLUDE_FROM_ALL SYSTEM) - +block () + set(BUILD_SHARED_LIBS OFF) + add_subdirectory("dependencies/openpnp-capture" EXCLUDE_FROM_ALL SYSTEM) +endblock() add_subdirectory(src) diff --git a/dependencies/openpnp-capture b/dependencies/openpnp-capture index b7765b1d..df8debe3 160000 --- a/dependencies/openpnp-capture +++ b/dependencies/openpnp-capture @@ -1 +1 @@ -Subproject commit b7765b1da5169c662d141534bb877b3665f11227 +Subproject commit df8debe3ecf68da06156be541dae78fe2517c14e From 03d16dcb8e2ed62e51f4f9c4046873c3697c8792 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Tue, 24 Feb 2026 00:52:12 +0000 Subject: [PATCH 09/28] Call `CAMExit` on camera lib unload --- src/Cafe/OS/libs/camera/camera.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Cafe/OS/libs/camera/camera.cpp b/src/Cafe/OS/libs/camera/camera.cpp index eba6c022..50176fd7 100644 --- a/src/Cafe/OS/libs/camera/camera.cpp +++ b/src/Cafe/OS/libs/camera/camera.cpp @@ -276,11 +276,6 @@ namespace camera s_instance.initialized = false; } - void reset() - { - CAMExit(0); - } - class : public COSModule { public: @@ -304,11 +299,11 @@ namespace camera { if (reason == coreinit::RplEntryReason::Loaded) { - reset(); + } else if (reason == coreinit::RplEntryReason::Unloaded) { - // todo + CAMExit(0); } } } s_COScameraModule; From 5bc7639930cfeba36e8ed6788ccc1aa82d1ef49d Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Tue, 24 Feb 2026 00:54:21 +0000 Subject: [PATCH 10/28] Update `openpnp-capture` (fix Windows static linking) --- dependencies/openpnp-capture | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/openpnp-capture b/dependencies/openpnp-capture index df8debe3..18591941 160000 --- a/dependencies/openpnp-capture +++ b/dependencies/openpnp-capture @@ -1 +1 @@ -Subproject commit df8debe3ecf68da06156be541dae78fe2517c14e +Subproject commit 18591941719746679709c3eff77404a5a240d7a1 From edc7fa48cc51341c184d64066e7646e73b1982ad Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Tue, 24 Feb 2026 02:48:33 +0000 Subject: [PATCH 11/28] Update `openpnp-capture` and set MSVC runtime lib --- CMakeLists.txt | 1 + dependencies/openpnp-capture | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 46866ac2..58552ab7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -254,5 +254,6 @@ endif() block () set(BUILD_SHARED_LIBS OFF) add_subdirectory("dependencies/openpnp-capture" EXCLUDE_FROM_ALL SYSTEM) + set_property(TARGET openpnp-capture PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") endblock() add_subdirectory(src) diff --git a/dependencies/openpnp-capture b/dependencies/openpnp-capture index 18591941..218bb145 160000 --- a/dependencies/openpnp-capture +++ b/dependencies/openpnp-capture @@ -1 +1 @@ -Subproject commit 18591941719746679709c3eff77404a5a240d7a1 +Subproject commit 218bb145dd0f00110726941bcff67e14700def6e From 25c71e04a25b98aa3acdb1848ed5d36b4ba816b4 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Tue, 24 Feb 2026 04:28:44 +0000 Subject: [PATCH 12/28] Update `openpnp-capture` (fix turbojpeg import) --- dependencies/openpnp-capture | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/openpnp-capture b/dependencies/openpnp-capture index 218bb145..283a17db 160000 --- a/dependencies/openpnp-capture +++ b/dependencies/openpnp-capture @@ -1 +1 @@ -Subproject commit 218bb145dd0f00110726941bcff67e14700def6e +Subproject commit 283a17db4b73fd174b9b98f9924d2d8456d16661 From e1031bddfc276a1f71a28052a0f399c05a20bc5a Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Tue, 24 Feb 2026 06:35:28 +0000 Subject: [PATCH 13/28] Update openpnp-capture (fix turbojpeg link failure when used alongside vcpkg) --- dependencies/openpnp-capture | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/openpnp-capture b/dependencies/openpnp-capture index 283a17db..0ddd7687 160000 --- a/dependencies/openpnp-capture +++ b/dependencies/openpnp-capture @@ -1 +1 @@ -Subproject commit 283a17db4b73fd174b9b98f9924d2d8456d16661 +Subproject commit 0ddd7687fe8479ad649f8101f172cb6c4f05b347 From 6ee1e1e3e4dffe5a02b55c626e93c1af7f6b27ab Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 00:21:57 +0100 Subject: [PATCH 14/28] Remove duplicate sources in `CMakeLists.txt` --- src/camera/CMakeLists.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/camera/CMakeLists.txt b/src/camera/CMakeLists.txt index 1584eb20..53e70501 100644 --- a/src/camera/CMakeLists.txt +++ b/src/camera/CMakeLists.txt @@ -4,10 +4,6 @@ add_library(CemuCamera CameraManager.h Rgb2Nv12.cpp Rgb2Nv12.h - CameraManager.h - CameraManager.cpp - Rgb2Nv12.h - Rgb2Nv12.cpp ) set_property(TARGET CemuCamera PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") From aa3c852560e1e5a875e03ebb5e7d59f34dbc91ba Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 00:56:54 +0100 Subject: [PATCH 15/28] Init `CameraManager` on open, deinit on close And buffer allocation and deallocation occur on init and deinit --- src/Cafe/OS/libs/camera/camera.cpp | 1 - src/camera/CameraManager.cpp | 101 ++++++++++++++----------- src/camera/CameraManager.h | 5 +- src/gui/wxgui/CameraSettingsWindow.cpp | 6 +- 4 files changed, 59 insertions(+), 54 deletions(-) diff --git a/src/Cafe/OS/libs/camera/camera.cpp b/src/Cafe/OS/libs/camera/camera.cpp index 50176fd7..02c2145d 100644 --- a/src/Cafe/OS/libs/camera/camera.cpp +++ b/src/Cafe/OS/libs/camera/camera.cpp @@ -190,7 +190,6 @@ namespace camera *error = CAM_STATUS_INVALID_ARG; return -1; } - CameraManager::Init(); cemu_assert_debug(initInfo->forceDisplay != CAMForceDisplay::DRC); cemu_assert_debug(initInfo->workMemorySize != 0); diff --git a/src/camera/CameraManager.cpp b/src/camera/CameraManager.cpp index 6a17efce..fb2fe11e 100644 --- a/src/camera/CameraManager.cpp +++ b/src/camera/CameraManager.cpp @@ -17,8 +17,8 @@ namespace CameraManager CapContext s_ctx; std::optional s_device; std::optional s_stream; - std::array s_rgbBuffer; - std::array s_nv12Buffer; + uint8_t* s_rgbBuffer; + uint8_t* s_nv12Buffer; int s_refCount = 0; std::thread s_captureThread; std::atomic_bool s_capturing = false; @@ -71,8 +71,8 @@ namespace CameraManager { s_mutex.lock(); if (s_stream && Cap_hasNewFrame(s_ctx, *s_stream) && - Cap_captureFrame(s_ctx, *s_stream, s_rgbBuffer.data(), s_rgbBuffer.size()) == CAPRESULT_OK) - Rgb2Nv12(s_rgbBuffer.data(), CAMERA_WIDTH, CAMERA_HEIGHT, s_nv12Buffer.data(), CAMERA_PITCH); + Cap_captureFrame(s_ctx, *s_stream, s_rgbBuffer, CAMERA_RGB_BUFFER_SIZE) == CAPRESULT_OK) + Rgb2Nv12(s_rgbBuffer, CAMERA_WIDTH, CAMERA_HEIGHT, s_nv12Buffer, CAMERA_PITCH); s_mutex.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(30)); } @@ -105,30 +105,49 @@ namespace CameraManager void ResetBuffers() { - std::ranges::fill(s_rgbBuffer, 0); - constexpr auto pixCount = CAMERA_HEIGHT * CAMERA_PITCH; - std::ranges::fill_n(s_nv12Buffer.begin(), pixCount, 16); - std::ranges::fill_n(s_nv12Buffer.begin() + pixCount, (pixCount / 2), 128); + std::fill_n(s_rgbBuffer, CAMERA_RGB_BUFFER_SIZE, 0); + constexpr static auto PIXEL_COUNT = CAMERA_HEIGHT * CAMERA_PITCH; + std::ranges::fill_n(s_nv12Buffer, PIXEL_COUNT, 16); + std::ranges::fill_n(s_nv12Buffer + PIXEL_COUNT, (PIXEL_COUNT / 2), 128); + } + + std::vector InternalEnumerateDevices() + { + std::vector infos; + const auto deviceCount = Cap_getDeviceCount(s_ctx); + cemuLog_log(LogType::InputAPI, "Available video capture devices:"); + for (CapDeviceID deviceNo = 0; deviceNo < deviceCount; ++deviceNo) + { + const auto uniqueId = Cap_getDeviceUniqueID(s_ctx, deviceNo); + const auto name = Cap_getDeviceName(s_ctx, deviceNo); + DeviceInfo info; + info.uniqueId = uniqueId; + + if (name) + info.name = fmt::format("{}: {}", deviceNo, name); + else + info.name = fmt::format("{}: Unknown", deviceNo); + infos.push_back(info); + cemuLog_log(LogType::InputAPI, "{}", info.name); + } + return infos; } void Init() { - { - std::scoped_lock lock(s_mutex); - if (s_running) - return; - s_running = true; - s_ctx = Cap_createContext(); - Cap_setLogLevel(4); - Cap_installCustomLogFunction(CaptureLogFunction); - } + s_running = true; + s_ctx = Cap_createContext(); + Cap_setLogLevel(4); + Cap_installCustomLogFunction(CaptureLogFunction); + s_rgbBuffer = new uint8[CAMERA_RGB_BUFFER_SIZE]; + s_nv12Buffer = new uint8[CAMERA_NV12_BUFFER_SIZE]; s_captureThread = std::thread(&CaptureWorker); const auto uniqueId = GetConfig().camera_id.GetValue(); if (!uniqueId.empty()) { - const auto devices = EnumerateDevices(); + const auto devices = InternalEnumerateDevices(); for (CapDeviceID deviceId = 0; deviceId < devices.size(); ++deviceId) { if (devices[deviceId].uniqueId == uniqueId) @@ -147,38 +166,46 @@ namespace CameraManager Cap_releaseContext(s_ctx); s_running = false; s_captureThread.join(); + delete[] s_rgbBuffer; + delete[] s_nv12Buffer; } void FillNV12Buffer(std::span nv12Buffer) { std::scoped_lock lock(s_mutex); - std::ranges::copy(s_nv12Buffer, nv12Buffer.data()); + std::ranges::copy_n(s_nv12Buffer, CAMERA_NV12_BUFFER_SIZE, nv12Buffer.data()); } void FillRGBBuffer(std::span rgbBuffer) { std::scoped_lock lock(s_mutex); - std::ranges::copy(s_rgbBuffer, rgbBuffer.data()); + std::ranges::copy_n(s_rgbBuffer, CAMERA_RGB_BUFFER_SIZE, rgbBuffer.data()); } void SetDevice(uint32 deviceNo) { std::scoped_lock lock(s_mutex); CloseStream(); - if (deviceNo == DEVICE_NONE) - { - s_device = std::nullopt; - ResetBuffers(); - return; - } s_device = deviceNo; if (s_refCount != 0) OpenStream(); } + void ResetDevice() + { + std::scoped_lock lock(s_mutex); + CloseStream(); + s_device = std::nullopt; + ResetBuffers(); + } + void Open() { std::scoped_lock lock(s_mutex); + if (!s_running) + { + Init(); + } if (s_device && s_refCount == 0) { OpenStream(); @@ -195,31 +222,13 @@ namespace CameraManager if (s_refCount != 0) return; CloseStream(); + Deinit(); } std::vector EnumerateDevices() { std::scoped_lock lock(s_mutex); - std::vector infos; - const auto deviceCount = Cap_getDeviceCount(s_ctx); - cemuLog_log(LogType::InputAPI, "Available video capture devices:"); - for (CapDeviceID deviceNo = 0; deviceNo < deviceCount; ++deviceNo) - { - const auto uniqueId = Cap_getDeviceUniqueID(s_ctx, deviceNo); - const auto name = Cap_getDeviceName(s_ctx, deviceNo); - DeviceInfo info; - info.uniqueId = uniqueId; - - if (name) - info.name = fmt::format("{}: {}", deviceNo, name); - else - info.name = fmt::format("{}: Unknown", deviceNo); - infos.push_back(info); - cemuLog_log(LogType::InputAPI, "{}", info.name); - } - if (infos.empty()) - cemuLog_log(LogType::InputAPI, "No available video capture devices"); - return infos; + return InternalEnumerateDevices(); } void SaveDevice() diff --git a/src/camera/CameraManager.h b/src/camera/CameraManager.h index 583fdbf1..dbe4f37f 100644 --- a/src/camera/CameraManager.h +++ b/src/camera/CameraManager.h @@ -18,10 +18,6 @@ namespace CameraManager std::string name; }; - constexpr static uint32 DEVICE_NONE = std::numeric_limits::max(); - - void Init(); - void Deinit(); void Open(); void Close(); @@ -29,6 +25,7 @@ namespace CameraManager void FillRGBBuffer(std::span rgbBuffer); void SetDevice(uint32 deviceNo); + void ResetDevice(); std::vector EnumerateDevices(); void SaveDevice(); std::optional GetCurrentDevice(); diff --git a/src/gui/wxgui/CameraSettingsWindow.cpp b/src/gui/wxgui/CameraSettingsWindow.cpp index a72cca23..f806b8e9 100644 --- a/src/gui/wxgui/CameraSettingsWindow.cpp +++ b/src/gui/wxgui/CameraSettingsWindow.cpp @@ -11,7 +11,6 @@ CameraSettingsWindow::CameraSettingsWindow(wxWindow* parent) m_imageBitmap(CameraManager::CAMERA_WIDTH, CameraManager::CAMERA_HEIGHT, 24), m_imageBuffer(CameraManager::CAMERA_RGB_BUFFER_SIZE) { - CameraManager::Init(); CameraManager::Open(); auto* rootSizer = new wxBoxSizer(wxVERTICAL); { @@ -51,7 +50,7 @@ void CameraSettingsWindow::OnSelectCameraChoice(wxCommandEvent&) if (selection < 0) return; if (selection == 0) - CameraManager::SetDevice(CameraManager::DEVICE_NONE); + CameraManager::ResetDevice(); else CameraManager::SetDevice(selection - 1); } @@ -100,7 +99,8 @@ void CameraSettingsWindow::DrawImage(const wxPaintEvent&) void CameraSettingsWindow::OnClose(wxCloseEvent& event) { - CameraManager::Close(); + m_imageUpdateTimer.Stop(); CameraManager::SaveDevice(); + CameraManager::Close(); event.Skip(); } From 9d1ebb2f73cf676c085ee84c430454a693224107 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 00:59:03 +0100 Subject: [PATCH 16/28] Remove unused variables --- src/Cafe/OS/libs/camera/camera.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Cafe/OS/libs/camera/camera.cpp b/src/Cafe/OS/libs/camera/camera.cpp index 02c2145d..fd6dce3d 100644 --- a/src/Cafe/OS/libs/camera/camera.cpp +++ b/src/Cafe/OS/libs/camera/camera.cpp @@ -99,10 +99,8 @@ namespace camera { std::recursive_mutex mutex{}; bool initialized = false; - bool shouldTriggerCallback = false; std::atomic_bool isOpen = false; std::atomic_bool isExiting = false; - bool isWorking = false; unsigned fps = 30; MEMPTR eventCallback = nullptr; RingBuffer, 20> inTargetBuffers{}; From 54d77952117659dd8675b030ae9089c61503b78d Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 01:15:57 +0100 Subject: [PATCH 17/28] Small changes to worker thread func --- src/Cafe/OS/libs/camera/camera.cpp | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Cafe/OS/libs/camera/camera.cpp b/src/Cafe/OS/libs/camera/camera.cpp index fd6dce3d..d3618f12 100644 --- a/src/Cafe/OS/libs/camera/camera.cpp +++ b/src/Cafe/OS/libs/camera/camera.cpp @@ -113,7 +113,7 @@ namespace camera SysAllocator> s_cameraWorkerThreadNameBuffer; SysAllocator s_cameraOpenEvent; - void WorkerThread(PPCInterpreter_t*) + static void WorkerThreadFunc(PPCInterpreter_t*) { s_cameraEventData->type = CAMEventType::Decode; s_cameraEventData->channel = 0; @@ -126,18 +126,12 @@ namespace camera coreinit::OSWaitEvent(s_cameraOpenEvent); while (true) { - if (!s_instance.isOpen || s_instance.isExiting) - { - // Fill leftover buffers before stopping - if (!s_instance.inTargetBuffers.HasData()) - break; - } - s_cameraEventData->type = CAMEventType::Decode; - s_cameraEventData->channel = 0; - const auto surfaceBuffer = s_instance.inTargetBuffers.Pop(); - if (surfaceBuffer.IsNull()) + 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; } @@ -149,6 +143,8 @@ namespace camera 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(Espresso::TIMER_CLOCK / (s_instance.fps - 1)); } @@ -202,7 +198,7 @@ namespace camera coreinit::OSEvent::EVENT_MODE::MODE_AUTO); coreinit::__OSCreateThreadType( - s_cameraWorkerThread, RPLLoader_MakePPCCallable(WorkerThread), 0, nullptr, + 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); From e586389ae99929c7cdb7b8dfbb159a2cb2e79909 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 01:46:14 +0100 Subject: [PATCH 18/28] Use `OSMutex` --- src/Cafe/OS/common/OSUtil.h | 11 +++++++++++ src/Cafe/OS/libs/camera/camera.cpp | 16 +++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) 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 d3618f12..db5e47d9 100644 --- a/src/Cafe/OS/libs/camera/camera.cpp +++ b/src/Cafe/OS/libs/camera/camera.cpp @@ -7,6 +7,7 @@ #include "Cafe/HW/Espresso/PPCCallback.h" #include "camera/CameraManager.h" #include "Common/CafeString.h" +#include "OS/common/OSUtil.h" #include "util/helpers/ringbuffer.h" namespace camera @@ -97,7 +98,6 @@ namespace camera struct { - std::recursive_mutex mutex{}; bool initialized = false; std::atomic_bool isOpen = false; std::atomic_bool isExiting = false; @@ -106,7 +106,7 @@ namespace camera RingBuffer, 20> inTargetBuffers{}; RingBuffer, 20> outTargetBuffers{}; } s_instance; - + SysAllocator s_cameraMutex; SysAllocator s_cameraEventData; SysAllocator s_cameraWorkerThread; SysAllocator s_cameraWorkerThreadStack; @@ -169,7 +169,7 @@ namespace camera sint32 CAMInit(uint32 cameraId, const CAMInitInfo_t* initInfo, betype* error) { *error = CAM_STATUS_SUCCESS; - std::scoped_lock lock(s_instance.mutex); + CafeLockGuard lock(s_cameraMutex); if (s_instance.initialized) { *error = CAM_STATUS_DEVICE_IN_USE; @@ -212,12 +212,10 @@ namespace camera { if (camHandle != CAM_HANDLE) return CAM_STATUS_INVALID_HANDLE; - { - std::scoped_lock lock(s_instance.mutex); + CafeLockGuard lock(s_cameraMutex); if (!s_instance.initialized || !s_instance.isOpen) return CAM_STATUS_UNINITIALIZED; s_instance.isOpen = false; - } CameraManager::Close(); return CAM_STATUS_SUCCESS; } @@ -226,7 +224,7 @@ namespace camera { if (camHandle != CAM_HANDLE) return CAM_STATUS_INVALID_HANDLE; - auto lock = std::scoped_lock(s_instance.mutex); + CafeLockGuard lock(s_cameraMutex); if (!s_instance.initialized) return CAM_STATUS_UNINITIALIZED; if (s_instance.isOpen) @@ -246,7 +244,7 @@ namespace camera if (!targetSurface || targetSurface->data.IsNull() || targetSurface->size < 1) return CAM_STATUS_INVALID_ARG; cemu_assert_debug(targetSurface->size >= CameraManager::CAMERA_NV12_BUFFER_SIZE); - auto lock = std::scoped_lock(s_instance.mutex); + CafeLockGuard lock(s_cameraMutex); if (!s_instance.initialized) return CAM_STATUS_UNINITIALIZED; if (!s_instance.inTargetBuffers.Push(targetSurface->data)) @@ -258,7 +256,7 @@ namespace camera { if (camHandle != CAM_HANDLE) return; - std::scoped_lock lock(s_instance.mutex); + CafeLockGuard lock(s_cameraMutex); if (!s_instance.initialized) return; s_instance.isExiting = true; From aff12540a6329bbc639ee5cb75dda8f633aa9b15 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 01:59:41 +0100 Subject: [PATCH 19/28] Formatting and cleanup --- src/Cafe/OS/libs/camera/camera.cpp | 7 ++--- src/camera/CameraManager.cpp | 44 ++++++++++++++-------------- src/gui/wxgui/CameraSettingsWindow.h | 29 +++++++++--------- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/src/Cafe/OS/libs/camera/camera.cpp b/src/Cafe/OS/libs/camera/camera.cpp index db5e47d9..a47c7072 100644 --- a/src/Cafe/OS/libs/camera/camera.cpp +++ b/src/Cafe/OS/libs/camera/camera.cpp @@ -213,9 +213,9 @@ namespace camera if (camHandle != CAM_HANDLE) return CAM_STATUS_INVALID_HANDLE; CafeLockGuard lock(s_cameraMutex); - if (!s_instance.initialized || !s_instance.isOpen) - return CAM_STATUS_UNINITIALIZED; - s_instance.isOpen = false; + if (!s_instance.initialized || !s_instance.isOpen) + return CAM_STATUS_UNINITIALIZED; + s_instance.isOpen = false; CameraManager::Close(); return CAM_STATUS_SUCCESS; } @@ -290,7 +290,6 @@ namespace camera { if (reason == coreinit::RplEntryReason::Loaded) { - } else if (reason == coreinit::RplEntryReason::Unloaded) { diff --git a/src/camera/CameraManager.cpp b/src/camera/CameraManager.cpp index fb2fe11e..89a44dd0 100644 --- a/src/camera/CameraManager.cpp +++ b/src/camera/CameraManager.cpp @@ -13,18 +13,18 @@ namespace CameraManager { - std::mutex s_mutex; - CapContext s_ctx; - std::optional s_device; - std::optional s_stream; - uint8_t* s_rgbBuffer; - uint8_t* s_nv12Buffer; - int s_refCount = 0; - std::thread s_captureThread; - std::atomic_bool s_capturing = false; - std::atomic_bool s_running = false; + static std::mutex s_mutex; + static CapContext s_ctx; + static std::optional s_device; + static std::optional s_stream; + static uint8_t* s_rgbBuffer; + static uint8_t* s_nv12Buffer; + static int s_refCount = 0; + static std::thread s_captureThread; + static std::atomic_bool s_capturing = false; + static std::atomic_bool s_running = false; - std::string FourCC(uint32le value) + static std::string FourCC(uint32le value) { return { static_cast((value >> 0) & 0xFF), @@ -34,12 +34,12 @@ namespace CameraManager }; } - void CaptureLogFunction(uint32_t level, const char* string) + static void CaptureLogFunction(uint32_t level, const char* string) { - cemuLog_log(LogType::InputAPI, "OpenPNPCapture: {}: {}", level, string); + cemuLog_log(LogType::InputAPI, "openpnp-capture: {}: {}", level, string); } - std::optional FindCorrectFormat() + static std::optional FindCorrectFormat() { const auto device = *s_device; cemuLog_log(LogType::InputAPI, "Video capture device '{}' available formats:", @@ -62,7 +62,7 @@ namespace CameraManager return std::nullopt; } - void CaptureWorker() + static void CaptureWorkerFunc() { SetThreadName("CameraManager"); while (s_running) @@ -81,7 +81,7 @@ namespace CameraManager } } - void OpenStream() + static void OpenStream() { const auto formatId = FindCorrectFormat(); if (!formatId) @@ -93,7 +93,7 @@ namespace CameraManager s_stream = stream; } - void CloseStream() + static void CloseStream() { s_capturing = false; if (s_stream) @@ -103,7 +103,7 @@ namespace CameraManager } } - void ResetBuffers() + static void ResetBuffers() { std::fill_n(s_rgbBuffer, CAMERA_RGB_BUFFER_SIZE, 0); constexpr static auto PIXEL_COUNT = CAMERA_HEIGHT * CAMERA_PITCH; @@ -111,7 +111,7 @@ namespace CameraManager std::ranges::fill_n(s_nv12Buffer + PIXEL_COUNT, (PIXEL_COUNT / 2), 128); } - std::vector InternalEnumerateDevices() + static std::vector InternalEnumerateDevices() { std::vector infos; const auto deviceCount = Cap_getDeviceCount(s_ctx); @@ -133,7 +133,7 @@ namespace CameraManager return infos; } - void Init() + static void Init() { s_running = true; s_ctx = Cap_createContext(); @@ -142,7 +142,7 @@ namespace CameraManager s_rgbBuffer = new uint8[CAMERA_RGB_BUFFER_SIZE]; s_nv12Buffer = new uint8[CAMERA_NV12_BUFFER_SIZE]; - s_captureThread = std::thread(&CaptureWorker); + s_captureThread = std::thread(&CaptureWorkerFunc); const auto uniqueId = GetConfig().camera_id.GetValue(); if (!uniqueId.empty()) @@ -160,7 +160,7 @@ namespace CameraManager ResetBuffers(); } - void Deinit() + static void Deinit() { CloseStream(); Cap_releaseContext(s_ctx); diff --git a/src/gui/wxgui/CameraSettingsWindow.h b/src/gui/wxgui/CameraSettingsWindow.h index 6fc29358..c61f0870 100644 --- a/src/gui/wxgui/CameraSettingsWindow.h +++ b/src/gui/wxgui/CameraSettingsWindow.h @@ -6,17 +6,18 @@ class CameraSettingsWindow : public wxDialog { - wxChoice* m_cameraChoice; - wxButton* m_refreshButton; - wxWindow* m_imageWindow; - wxBitmap m_imageBitmap; - wxTimer m_imageUpdateTimer; - std::vector m_imageBuffer; - public: - explicit CameraSettingsWindow(wxWindow* parent); - void OnSelectCameraChoice(wxCommandEvent&); - void OnRefreshPressed(wxCommandEvent&); - void UpdateImage(const wxTimerEvent&); - void DrawImage(const wxPaintEvent&); - void OnClose(wxCloseEvent& event); -}; \ No newline at end of file + wxChoice* m_cameraChoice; + wxButton* m_refreshButton; + wxWindow* m_imageWindow; + wxBitmap m_imageBitmap; + wxTimer m_imageUpdateTimer; + std::vector m_imageBuffer; + +public: + explicit CameraSettingsWindow(wxWindow* parent); + void OnSelectCameraChoice(wxCommandEvent&); + void OnRefreshPressed(wxCommandEvent&); + void UpdateImage(const wxTimerEvent&); + void DrawImage(const wxPaintEvent&); + void OnClose(wxCloseEvent& event); +}; From cf6781fdcb20c6e01c9c1f720f255c7ed0cad12a Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 02:21:30 +0100 Subject: [PATCH 20/28] `CMakeLists.txt` adjustments --- src/camera/CMakeLists.txt | 6 +++++- src/gui/wxgui/CMakeLists.txt | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/camera/CMakeLists.txt b/src/camera/CMakeLists.txt index 53e70501..ca5d43f2 100644 --- a/src/camera/CMakeLists.txt +++ b/src/camera/CMakeLists.txt @@ -9,4 +9,8 @@ add_library(CemuCamera set_property(TARGET CemuCamera PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") target_include_directories(CemuCamera PUBLIC "../") -target_link_libraries(CemuCamera PRIVATE CemuCommon CemuUtil openpnp-capture) +target_link_libraries(CemuCamera PRIVATE + CemuCommon + CemuUtil + glm::glm + openpnp-capture) diff --git a/src/gui/wxgui/CMakeLists.txt b/src/gui/wxgui/CMakeLists.txt index c8ef44fa..72b4bef9 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 canvas/OpenGLCanvas.cpp canvas/OpenGLCanvas.h @@ -114,8 +116,6 @@ add_library(CemuWxGui STATIC wxcomponents/checktree.h wxgui.h wxHelper.h - CameraSettingsWindow.cpp - CameraSettingsWindow.h ) if (ENABLE_METAL) @@ -150,6 +150,7 @@ target_link_libraries(CemuWxGui PRIVATE SDL2::SDL2 pugixml::pugixml CemuCafe + CemuCamera PUBLIC CURL::libcurl ) From 756e79c51ede25491b6f2fe28e8246c2135e7fac Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 02:32:10 +0100 Subject: [PATCH 21/28] Remove extra `#pragma once` --- src/camera/CameraManager.h | 3 --- src/camera/Rgb2Nv12.h | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/camera/CameraManager.h b/src/camera/CameraManager.h index dbe4f37f..54eaaede 100644 --- a/src/camera/CameraManager.h +++ b/src/camera/CameraManager.h @@ -1,8 +1,5 @@ #pragma once -#pragma once -#include - namespace CameraManager { constexpr uint32 CAMERA_WIDTH = 640; diff --git a/src/camera/Rgb2Nv12.h b/src/camera/Rgb2Nv12.h index 42178809..d0e01d01 100644 --- a/src/camera/Rgb2Nv12.h +++ b/src/camera/Rgb2Nv12.h @@ -1,7 +1,5 @@ #pragma once -#pragma once - void Rgb2Nv12(const uint8* rgbImage, unsigned imageWidth, unsigned imageHeight, From 2f6a13650e8b2450a882a0f6d15d48146ea19b30 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 02:45:00 +0100 Subject: [PATCH 22/28] Link to `CemuCamera` in `CemuBin` --- src/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4553fe31..3bba5eb3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -161,6 +161,7 @@ set_target_properties(CemuBin PROPERTIES target_link_libraries(CemuBin PRIVATE CemuAudio CemuCafe + CemuCamera CemuCommon CemuComponents CemuConfig From 1bb26db454596ba24b1ff3cfce1cdcbe261550e8 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 03:20:36 +0100 Subject: [PATCH 23/28] Link `CemuCommon` as `PUBLIC` I know it doesn't match the rest of the `CMakeLists` --- src/camera/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/camera/CMakeLists.txt b/src/camera/CMakeLists.txt index ca5d43f2..d805eace 100644 --- a/src/camera/CMakeLists.txt +++ b/src/camera/CMakeLists.txt @@ -9,8 +9,10 @@ add_library(CemuCamera set_property(TARGET CemuCamera PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") target_include_directories(CemuCamera PUBLIC "../") -target_link_libraries(CemuCamera PRIVATE +target_link_libraries(CemuCamera + PUBLIC CemuCommon + PRIVATE CemuUtil glm::glm openpnp-capture) From 49c33981a719fd614dccf4ef87a75d590fc1f814 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 03:27:06 +0100 Subject: [PATCH 24/28] Link to `CemuGui` in `CemuCamera` despite not using any GUI code Every other Cemu component links it --- src/camera/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/camera/CMakeLists.txt b/src/camera/CMakeLists.txt index d805eace..ce5aa8a8 100644 --- a/src/camera/CMakeLists.txt +++ b/src/camera/CMakeLists.txt @@ -10,9 +10,9 @@ set_property(TARGET CemuCamera PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$ Date: Wed, 15 Apr 2026 03:45:28 +0100 Subject: [PATCH 25/28] Give up on using the precompiled headers --- src/camera/CMakeLists.txt | 2 -- src/camera/CameraManager.cpp | 2 +- src/camera/CameraManager.h | 23 ++++++++++++++--------- src/camera/Rgb2Nv12.cpp | 17 +++++++++-------- src/camera/Rgb2Nv12.h | 5 +++-- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/camera/CMakeLists.txt b/src/camera/CMakeLists.txt index ce5aa8a8..ee846929 100644 --- a/src/camera/CMakeLists.txt +++ b/src/camera/CMakeLists.txt @@ -11,8 +11,6 @@ set_property(TARGET CemuCamera PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$ +#include #include -#include #include #include diff --git a/src/camera/CameraManager.h b/src/camera/CameraManager.h index 54eaaede..b6181c0d 100644 --- a/src/camera/CameraManager.h +++ b/src/camera/CameraManager.h @@ -1,13 +1,18 @@ #pragma once +#include +#include +#include +#include +#include namespace CameraManager { - constexpr uint32 CAMERA_WIDTH = 640; - constexpr uint32 CAMERA_HEIGHT = 480; - constexpr uint32 CAMERA_PITCH = 768; + constexpr uint32_t CAMERA_WIDTH = 640; + constexpr uint32_t CAMERA_HEIGHT = 480; + constexpr uint32_t 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; + constexpr uint32_t CAMERA_NV12_BUFFER_SIZE = (CAMERA_HEIGHT * CAMERA_PITCH * 3) >> 1; + constexpr uint32_t CAMERA_RGB_BUFFER_SIZE = CAMERA_HEIGHT * CAMERA_WIDTH * 3; struct DeviceInfo { @@ -18,12 +23,12 @@ namespace CameraManager void Open(); void Close(); - void FillNV12Buffer(std::span nv12Buffer); - void FillRGBBuffer(std::span rgbBuffer); + void FillNV12Buffer(std::span nv12Buffer); + void FillRGBBuffer(std::span rgbBuffer); - void SetDevice(uint32 deviceNo); + void SetDevice(uint32_t deviceNo); void ResetDevice(); std::vector EnumerateDevices(); void SaveDevice(); - std::optional GetCurrentDevice(); + std::optional GetCurrentDevice(); } // namespace CameraManager diff --git a/src/camera/Rgb2Nv12.cpp b/src/camera/Rgb2Nv12.cpp index 7b599309..ab914f81 100644 --- a/src/camera/Rgb2Nv12.cpp +++ b/src/camera/Rgb2Nv12.cpp @@ -1,5 +1,7 @@ // Based on https://github.com/cohenrotem/Rgb2NV12 #include "Rgb2Nv12.h" +#include +#include constexpr static glm::mat3x3 COEFFICIENT_MATRIX = { @@ -13,12 +15,12 @@ constexpr static glm::mat4x3 OFFSET_MATRIX = { 16.0f + 0.5f, 128.0f + 2.0f, 128.0f + 2.0f, 16.0f + 0.5f, 128.0f + 2.0f, 128.0f + 2.0f}; -static void Rgb2Nv12TwoRows(const uint8* topLine, - const uint8* bottomLine, +static void Rgb2Nv12TwoRows(const uint8_t* topLine, + const uint8_t* bottomLine, unsigned imageWidth, - uint8* topLineY, - uint8* bottomLineY, - uint8* uv) + uint8_t* topLineY, + uint8_t* bottomLineY, + uint8_t* uv) { auto* topIn = reinterpret_cast(topLine); auto* botIn = reinterpret_cast(bottomLine); @@ -44,13 +46,12 @@ static void Rgb2Nv12TwoRows(const uint8* topLine, } } -void Rgb2Nv12(const uint8* rgbImage, +void Rgb2Nv12(const uint8_t* rgbImage, unsigned imageWidth, unsigned imageHeight, - uint8* outNv12Image, + uint8_t* outNv12Image, unsigned nv12Pitch) { - cemu_assert_debug(!((imageWidth | imageHeight) & 1)); unsigned char* UV = outNv12Image + nv12Pitch * imageHeight; for (auto row = 0u; row < imageHeight; row += 2) diff --git a/src/camera/Rgb2Nv12.h b/src/camera/Rgb2Nv12.h index d0e01d01..ea73a3f1 100644 --- a/src/camera/Rgb2Nv12.h +++ b/src/camera/Rgb2Nv12.h @@ -1,7 +1,8 @@ #pragma once +#include -void Rgb2Nv12(const uint8* rgbImage, +void Rgb2Nv12(const uint8_t* rgbImage, unsigned imageWidth, unsigned imageHeight, - uint8* outNv12Image, + uint8_t* outNv12Image, unsigned nv12Pitch); \ No newline at end of file From a4478db4b3befc7b0cea8fed0208e390f4bd2230 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 03:55:31 +0100 Subject: [PATCH 26/28] Revert "Give up on using the precompiled headers" This reverts commit 9fb8618c442080681acee28822a1e88e12b73573. --- src/camera/CMakeLists.txt | 2 ++ src/camera/CameraManager.cpp | 2 +- src/camera/CameraManager.h | 23 +++++++++-------------- src/camera/Rgb2Nv12.cpp | 17 ++++++++--------- src/camera/Rgb2Nv12.h | 5 ++--- 5 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/camera/CMakeLists.txt b/src/camera/CMakeLists.txt index ee846929..ce5aa8a8 100644 --- a/src/camera/CMakeLists.txt +++ b/src/camera/CMakeLists.txt @@ -11,6 +11,8 @@ set_property(TARGET CemuCamera PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$ -#include #include +#include #include #include diff --git a/src/camera/CameraManager.h b/src/camera/CameraManager.h index b6181c0d..54eaaede 100644 --- a/src/camera/CameraManager.h +++ b/src/camera/CameraManager.h @@ -1,18 +1,13 @@ #pragma once -#include -#include -#include -#include -#include namespace CameraManager { - constexpr uint32_t CAMERA_WIDTH = 640; - constexpr uint32_t CAMERA_HEIGHT = 480; - constexpr uint32_t CAMERA_PITCH = 768; + constexpr uint32 CAMERA_WIDTH = 640; + constexpr uint32 CAMERA_HEIGHT = 480; + constexpr uint32 CAMERA_PITCH = 768; - constexpr uint32_t CAMERA_NV12_BUFFER_SIZE = (CAMERA_HEIGHT * CAMERA_PITCH * 3) >> 1; - constexpr uint32_t CAMERA_RGB_BUFFER_SIZE = CAMERA_HEIGHT * CAMERA_WIDTH * 3; + 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 { @@ -23,12 +18,12 @@ namespace CameraManager void Open(); void Close(); - void FillNV12Buffer(std::span nv12Buffer); - void FillRGBBuffer(std::span rgbBuffer); + void FillNV12Buffer(std::span nv12Buffer); + void FillRGBBuffer(std::span rgbBuffer); - void SetDevice(uint32_t deviceNo); + void SetDevice(uint32 deviceNo); void ResetDevice(); std::vector EnumerateDevices(); void SaveDevice(); - std::optional GetCurrentDevice(); + std::optional GetCurrentDevice(); } // namespace CameraManager diff --git a/src/camera/Rgb2Nv12.cpp b/src/camera/Rgb2Nv12.cpp index ab914f81..7b599309 100644 --- a/src/camera/Rgb2Nv12.cpp +++ b/src/camera/Rgb2Nv12.cpp @@ -1,7 +1,5 @@ // Based on https://github.com/cohenrotem/Rgb2NV12 #include "Rgb2Nv12.h" -#include -#include constexpr static glm::mat3x3 COEFFICIENT_MATRIX = { @@ -15,12 +13,12 @@ constexpr static glm::mat4x3 OFFSET_MATRIX = { 16.0f + 0.5f, 128.0f + 2.0f, 128.0f + 2.0f, 16.0f + 0.5f, 128.0f + 2.0f, 128.0f + 2.0f}; -static void Rgb2Nv12TwoRows(const uint8_t* topLine, - const uint8_t* bottomLine, +static void Rgb2Nv12TwoRows(const uint8* topLine, + const uint8* bottomLine, unsigned imageWidth, - uint8_t* topLineY, - uint8_t* bottomLineY, - uint8_t* uv) + uint8* topLineY, + uint8* bottomLineY, + uint8* uv) { auto* topIn = reinterpret_cast(topLine); auto* botIn = reinterpret_cast(bottomLine); @@ -46,12 +44,13 @@ static void Rgb2Nv12TwoRows(const uint8_t* topLine, } } -void Rgb2Nv12(const uint8_t* rgbImage, +void Rgb2Nv12(const uint8* rgbImage, unsigned imageWidth, unsigned imageHeight, - uint8_t* outNv12Image, + uint8* outNv12Image, unsigned nv12Pitch) { + cemu_assert_debug(!((imageWidth | imageHeight) & 1)); unsigned char* UV = outNv12Image + nv12Pitch * imageHeight; for (auto row = 0u; row < imageHeight; row += 2) diff --git a/src/camera/Rgb2Nv12.h b/src/camera/Rgb2Nv12.h index ea73a3f1..d0e01d01 100644 --- a/src/camera/Rgb2Nv12.h +++ b/src/camera/Rgb2Nv12.h @@ -1,8 +1,7 @@ #pragma once -#include -void Rgb2Nv12(const uint8_t* rgbImage, +void Rgb2Nv12(const uint8* rgbImage, unsigned imageWidth, unsigned imageHeight, - uint8_t* outNv12Image, + uint8* outNv12Image, unsigned nv12Pitch); \ No newline at end of file From df55484d0966065f1f00813925392f9dc948ffba Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 04:00:25 +0100 Subject: [PATCH 27/28] Link to `CemuConfig` in `CemuCamera` I'm using it after all --- src/camera/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/camera/CMakeLists.txt b/src/camera/CMakeLists.txt index ce5aa8a8..852a95ab 100644 --- a/src/camera/CMakeLists.txt +++ b/src/camera/CMakeLists.txt @@ -12,7 +12,7 @@ target_include_directories(CemuCamera PUBLIC "../") target_link_libraries(CemuCamera PRIVATE CemuCommon - CemuGui + CemuConfig CemuUtil glm::glm openpnp-capture) From 8acf6f621abe5fa3fb5df247cf6820aa4333f1ce Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Wed, 15 Apr 2026 15:18:58 +0100 Subject: [PATCH 28/28] Use `cemu_use_precompiled_header` --- src/camera/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/camera/CMakeLists.txt b/src/camera/CMakeLists.txt index 852a95ab..6fbe1764 100644 --- a/src/camera/CMakeLists.txt +++ b/src/camera/CMakeLists.txt @@ -1,4 +1,3 @@ - add_library(CemuCamera CameraManager.cpp CameraManager.h @@ -6,6 +5,8 @@ add_library(CemuCamera Rgb2Nv12.h ) +cemu_use_precompiled_header(CemuCamera) + set_property(TARGET CemuCamera PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") target_include_directories(CemuCamera PUBLIC "../")