From b9000bd66738f2827e96f5114db0ebb86a9daf82 Mon Sep 17 00:00:00 2001 From: capitalistspz Date: Sun, 30 Mar 2025 03:03:49 +0100 Subject: [PATCH] Add camera settings window and save camera config --- src/Cafe/OS/libs/camera/camera.cpp | 3 +- src/camera/CMakeLists.txt | 6 +- src/camera/CameraManager.cpp | 167 ++++++++++++++++++++--------- src/camera/CameraManager.h | 25 ++++- src/config/CemuConfig.cpp | 6 ++ src/config/CemuConfig.h | 3 + src/gui/CMakeLists.txt | 3 + src/gui/CameraSettingsWindow.cpp | 98 +++++++++++++++++ src/gui/CameraSettingsWindow.h | 22 ++++ src/gui/MainWindow.cpp | 10 ++ 10 files changed, 284 insertions(+), 59 deletions(-) create mode 100644 src/gui/CameraSettingsWindow.cpp create mode 100644 src/gui/CameraSettingsWindow.h diff --git a/src/Cafe/OS/libs/camera/camera.cpp b/src/Cafe/OS/libs/camera/camera.cpp index cfdd01df..90f1b073 100644 --- a/src/Cafe/OS/libs/camera/camera.cpp +++ b/src/Cafe/OS/libs/camera/camera.cpp @@ -234,8 +234,7 @@ namespace camera return CAM_STATUS_UNINITIALIZED; if (s_instance.isOpen) return CAM_STATUS_DEVICE_IN_USE; - if (!CameraManager::instance().Open(false)) - return CAM_STATUS_UVC_ERROR; + CameraManager::instance().Open(); s_instance.isOpen = true; coreinit::OSSignalEvent(s_cameraOpenEvent); s_instance.inTargetBuffers.Clear(); diff --git a/src/camera/CMakeLists.txt b/src/camera/CMakeLists.txt index 8a8e96f3..44342018 100644 --- a/src/camera/CMakeLists.txt +++ b/src/camera/CMakeLists.txt @@ -8,4 +8,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 PUBLIC openpnp-capture) \ No newline at end of file +target_link_libraries(CemuCamera PRIVATE CemuCommon CemuUtil PUBLIC openpnp-capture) + +if (ENABLE_WXWIDGETS) + target_link_libraries(CemuCamera PRIVATE wx::base) +endif() \ No newline at end of file diff --git a/src/camera/CameraManager.cpp b/src/camera/CameraManager.cpp index d0d4be11..4dfa52c2 100644 --- a/src/camera/CameraManager.cpp +++ b/src/camera/CameraManager.cpp @@ -1,5 +1,6 @@ #include "CameraManager.h" #include "Rgb2Nv12.h" +#include "config/CemuConfig.h" #include "util/helpers/helpers.h" constexpr unsigned CAMERA_WIDTH = 640; @@ -10,93 +11,157 @@ CameraManager::CameraManager() : m_ctx(Cap_createContext()), m_rgbBuffer(CAMERA_WIDTH * CAMERA_HEIGHT * 3), m_nv12Buffer(CAMERA_PITCH * CAMERA_HEIGHT * 3 / 2), - m_refCount(0) + m_refCount(0), m_capturing(false), m_running(true) { - // Set default device if it exists - if (Cap_getDeviceCount(m_ctx) > 0) - m_device = 0; + m_captureThread = std::thread(&CameraManager::CaptureWorker, this); + + 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) + { + m_device = deviceId; + return; + } + } + } + ResetBuffers(); } CameraManager::~CameraManager() { - Close(); + m_running = false; + CloseStream(); Cap_releaseContext(m_ctx); } void CameraManager::SetDevice(uint32 deviceNo) { std::scoped_lock lock(m_mutex); - if (m_device == deviceNo) + CloseStream(); + if (deviceNo == DEVICE_NONE) + { + m_device = std::nullopt; + ResetBuffers(); return; - if (m_stream) - Cap_closeStream(m_ctx, *m_stream); + } m_device = deviceNo; + if (m_refCount != 0) + OpenStream(); } -bool CameraManager::Open(bool weak) +std::vector CameraManager::EnumerateDevices() { std::scoped_lock lock(m_mutex); - if (!m_device) - return false; - if (m_refCount == 0) + std::vector infos; + const auto deviceCount = Cap_getDeviceCount(m_ctx); + for (CapDeviceID deviceNo = 0; deviceNo < deviceCount; ++deviceNo) { - const auto formatCount = Cap_getNumFormats(m_ctx, *m_device); - CapFormatID formatId = 0; - for (; formatId < formatCount; ++formatId) - { - CapFormatInfo formatInfo; - if (Cap_getFormatInfo(m_ctx, *m_device, formatId, &formatInfo) != CAPRESULT_OK) - continue; - if (formatInfo.width == CAMERA_WIDTH && formatInfo.height == CAMERA_HEIGHT) - break; - } - if (formatId == formatCount) - return false; - auto stream = Cap_openStream(m_ctx, *m_device, formatId); - if (stream == -1) - return false; - m_capturing = true; - m_stream = stream; - m_captureThread = std::thread(&CameraManager::CaptureWorker, this); + const auto uniqueId = Cap_getDeviceUniqueID(m_ctx, deviceNo); + const auto name = Cap_getDeviceName(m_ctx, deviceNo); + + if (name) + infos.emplace_back(uniqueId, fmt::format("{}: {}", deviceNo + 1, name)); + else + infos.emplace_back(uniqueId, fmt::format("{}: Unknown", deviceNo + 1)); } - if (!weak) - m_refCount += 1; - return true; + return infos; +} +void CameraManager::SaveDevice() +{ + std::scoped_lock lock(m_mutex); + if (m_device) + GetConfig().camera_id = Cap_getDeviceUniqueID(m_ctx, *m_device); + else + GetConfig().camera_id = ""; +} +void CameraManager::Open() +{ + std::scoped_lock lock(m_mutex); + if (m_device && m_refCount == 0) + { + OpenStream(); + } + m_refCount += 1; } void CameraManager::Close() { + std::scoped_lock lock(m_mutex); + if (m_refCount == 0) + return; + m_refCount -= 1; + if (m_refCount != 0) + return; + CloseStream(); +} + +std::optional CameraManager::FindCorrectFormat() +{ + const auto formatCount = Cap_getNumFormats(m_ctx, *m_device); + for (CapFormatID formatId = 0; formatId < formatCount; ++formatId) { - std::scoped_lock lock(m_mutex); - if (m_refCount == 0) - return; - m_refCount -= 1; - if (m_refCount != 0) - return; - Cap_closeStream(m_ctx, *m_stream); - m_stream = std::nullopt; - m_capturing = false; + CapFormatInfo formatInfo; + if (Cap_getFormatInfo(m_ctx, *m_device, formatId, &formatInfo) != CAPRESULT_OK) + continue; + if (formatInfo.width == CAMERA_WIDTH && formatInfo.height == CAMERA_HEIGHT) + return formatId; } - m_captureThread.join(); + return std::nullopt; +} +void CameraManager::ResetBuffers() +{ + std::ranges::fill(m_rgbBuffer, 0); + std::ranges::fill_n(m_nv12Buffer.begin(), CAMERA_WIDTH * CAMERA_PITCH, 16); + std::ranges::fill_n(m_nv12Buffer.begin() + CAMERA_WIDTH * CAMERA_PITCH, (CAMERA_WIDTH / 2), 128); } void CameraManager::FillNV12Buffer(uint8* nv12Buffer) const { - std::shared_lock lock(m_mutex); + std::scoped_lock lock(m_mutex); std::ranges::copy(m_nv12Buffer, nv12Buffer); } + +void CameraManager::FillRGBBuffer(uint8* rgbBuffer) const +{ + std::scoped_lock lock(m_mutex); + std::ranges::copy(m_rgbBuffer, rgbBuffer); +} void CameraManager::CaptureWorker() { SetThreadName("CameraManager"); - while (m_capturing) + while (m_running) { + while (m_capturing) { - std::scoped_lock lock(m_mutex); - bool frameAvailable = Cap_hasNewFrame(m_ctx, *m_stream); - if (frameAvailable && + m_mutex.lock(); + if (m_stream && Cap_hasNewFrame(m_ctx, *m_stream) && Cap_captureFrame(m_ctx, *m_stream, m_rgbBuffer.data(), m_rgbBuffer.size()) == CAPRESULT_OK) - { Rgb2Nv12(m_rgbBuffer.data(), CAMERA_WIDTH, CAMERA_HEIGHT, m_nv12Buffer.data(), CAMERA_PITCH); - } + m_mutex.unlock(); + std::this_thread::sleep_for(std::chrono::milliseconds(30)); } - std::this_thread::sleep_for(std::chrono::milliseconds(16)); + std::this_thread::sleep_for(std::chrono::seconds(1)); std::this_thread::yield(); } +} +void CameraManager::OpenStream() +{ + const auto formatId = FindCorrectFormat(); + if (!formatId) + return; + const auto stream = Cap_openStream(m_ctx, *m_device, *formatId); + if (stream == -1) + return; + m_capturing = true; + m_stream = stream; +} +void CameraManager::CloseStream() +{ + m_capturing = false; + if (m_stream) + { + Cap_closeStream(m_ctx, *m_stream); + m_stream = std::nullopt; + } } \ No newline at end of file diff --git a/src/camera/CameraManager.h b/src/camera/CameraManager.h index 601ab44f..e8e0c971 100644 --- a/src/camera/CameraManager.h +++ b/src/camera/CameraManager.h @@ -1,5 +1,7 @@ #pragma once -#include +#include +#include +#include #include #include "util/helpers/Singleton.h" @@ -13,19 +15,32 @@ class CameraManager : public Singleton int m_refCount; std::thread m_captureThread; std::atomic_bool m_capturing; - mutable std::shared_mutex m_mutex; + std::atomic_bool m_running; + mutable std::recursive_mutex m_mutex; public: + constexpr static uint32 DEVICE_NONE = std::numeric_limits::max(); + struct DeviceInfo + { + std::string uniqueId; + std::string name; + }; CameraManager(); ~CameraManager(); void SetDevice(uint32 deviceNo); + std::vector EnumerateDevices(); + void SaveDevice(); - bool Open(bool weak); + void Open(); void Close(); - void FillNV12Buffer(uint8* nv12Buffer) const; + void FillRGBBuffer(uint8* rgbBuffer) const; private: + std::optional FindCorrectFormat(); + void ResetBuffers(); void CaptureWorker(); -}; \ No newline at end of file + void OpenStream(); + void CloseStream(); +}; diff --git a/src/config/CemuConfig.cpp b/src/config/CemuConfig.cpp index 6bb7ac34..95ef42b2 100644 --- a/src/config/CemuConfig.cpp +++ b/src/config/CemuConfig.cpp @@ -343,6 +343,9 @@ void CemuConfig::Load(XMLConfigParser& parser) dsu_client.host = dsuc.get_attribute("host", dsu_client.host); dsu_client.port = dsuc.get_attribute("port", dsu_client.port); + auto camera = parser.get("Camera"); + camera_id = camera.get_attribute("Id", ""); + // emulatedusbdevices auto usbdevices = parser.get("EmulatedUsbDevices"); emulated_usb_devices.emulate_skylander_portal = usbdevices.get("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal); @@ -544,6 +547,9 @@ void CemuConfig::Save(XMLConfigParser& parser) dsuc.set_attribute("host", dsu_client.host); dsuc.set_attribute("port", dsu_client.port); + auto camera = config.set("Camera"); + camera.set("Id", camera_id); + // emulated usb devices auto usbdevices = config.set("EmulatedUsbDevices"); usbdevices.set("EmulateSkylanderPortal", emulated_usb_devices.emulate_skylander_portal.GetValue()); diff --git a/src/config/CemuConfig.h b/src/config/CemuConfig.h index 62665f6d..f429a438 100644 --- a/src/config/CemuConfig.h +++ b/src/config/CemuConfig.h @@ -499,6 +499,9 @@ struct CemuConfig ConfigValue port{ 26760 }; }dsu_client{}; + // camera + ConfigValue camera_id; + // debug ConfigValueBounds crash_dump{ CrashDump::Disabled }; ConfigValue gdb_port{ 1337 }; diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 7cdc208e..aa302fdf 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -127,6 +127,8 @@ add_library(CemuGui wxcomponents/unchecked.xpm wxgui.h wxHelper.h + CameraSettingsWindow.cpp + CameraSettingsWindow.h ) set_property(TARGET CemuGui PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") @@ -139,6 +141,7 @@ target_include_directories(CemuGui PUBLIC ${RAPIDJSON_INCLUDE_DIRS}) target_link_libraries(CemuGui PRIVATE CemuAudio CemuCafe + CemuCamera CemuCommon CemuComponents CemuConfig diff --git a/src/gui/CameraSettingsWindow.cpp b/src/gui/CameraSettingsWindow.cpp new file mode 100644 index 00000000..e73d9211 --- /dev/null +++ b/src/gui/CameraSettingsWindow.cpp @@ -0,0 +1,98 @@ +#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) +{ + + + auto* rootSizer = new wxBoxSizer(wxVERTICAL); + { + auto* topSizer = new wxBoxSizer(wxHORIZONTAL); + { + wxString choices[] = {_("None")}; + m_cameraChoice = new wxChoice(this, wxID_ANY, wxDefaultPosition, {300, -1}, 1, choices); + 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); + CameraManager::instance().Open(); + 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::instance().SetDevice(CameraManager::DEVICE_NONE); + else + CameraManager::instance().SetDevice(selection - 1); +} +void CameraSettingsWindow::OnRefreshPressed(wxCommandEvent&) +{ + wxArrayString choices = {_("None")}; + for (const auto& entry : CameraManager::instance().EnumerateDevices()) + { + choices.push_back(entry.name); + } + m_cameraChoice->Set(choices); + wxArrayString str; + +} +void CameraSettingsWindow::UpdateImage(const wxTimerEvent&) +{ + CameraManager::instance().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::instance().Close(); + CameraManager::instance().SaveDevice(); + event.Skip(); +} diff --git a/src/gui/CameraSettingsWindow.h b/src/gui/CameraSettingsWindow.h new file mode 100644 index 00000000..86014d65 --- /dev/null +++ b/src/gui/CameraSettingsWindow.h @@ -0,0 +1,22 @@ +#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); +}; + diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 48bdd7d7..16811faa 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -61,6 +61,7 @@ #include "gamemode_client.h" #endif +#include "CameraSettingsWindow.h" #include "Cafe/TitleList/TitleInfo.h" #include "Cafe/TitleList/TitleList.h" #include "wxHelper.h" @@ -91,6 +92,7 @@ enum MAINFRAME_MENU_ID_OPTIONS_GENERAL2, MAINFRAME_MENU_ID_OPTIONS_AUDIO, MAINFRAME_MENU_ID_OPTIONS_INPUT, + MAINFRAME_MENU_ID_OPTIONS_CAMERA, // options -> account MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_1 = 20350, MAINFRAME_MENU_ID_OPTIONS_ACCOUNT_12 = 20350 + 11, @@ -186,6 +188,7 @@ EVT_MENU(MAINFRAME_MENU_ID_OPTIONS_GENERAL, MainWindow::OnOptionsInput) 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_CAMERA, MainWindow::OnOptionsInput) // tools menu EVT_MENU(MAINFRAME_MENU_ID_TOOLS_MEMORY_SEARCHER, MainWindow::OnToolsInput) EVT_MENU(MAINFRAME_MENU_ID_TOOLS_TITLE_MANAGER, MainWindow::OnToolsInput) @@ -921,6 +924,12 @@ void MainWindow::OnOptionsInput(wxCommandEvent& event) frame->Destroy(); break; } + case MAINFRAME_MENU_ID_OPTIONS_CAMERA: + { + auto* frame = new CameraSettingsWindow(this); + frame->ShowModal(); + frame->Destroy(); + } } } @@ -2159,6 +2168,7 @@ void MainWindow::RecreateMenu() optionsMenu->AppendSeparator(); optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_GENERAL2, _("&General settings")); optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_INPUT, _("&Input settings")); + optionsMenu->Append(MAINFRAME_MENU_ID_OPTIONS_CAMERA, _("&Camera settings")); optionsMenu->AppendSeparator(); optionsMenu->AppendSubMenu(m_optionsAccountMenu, _("&Active account"));