Add camera settings window and save camera config

This commit is contained in:
capitalistspz 2025-03-30 03:03:49 +01:00
parent 791e358bf7
commit b9000bd667
10 changed files with 284 additions and 59 deletions

View file

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

View file

@ -8,4 +8,8 @@ add_library(CemuCamera
set_property(TARGET CemuCamera PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
target_include_directories(CemuCamera PUBLIC "../")
target_link_libraries(CemuCamera PRIVATE CemuCommon CemuUtil PUBLIC openpnp-capture)
target_link_libraries(CemuCamera PRIVATE CemuCommon CemuUtil PUBLIC openpnp-capture)
if (ENABLE_WXWIDGETS)
target_link_libraries(CemuCamera PRIVATE wx::base)
endif()

View file

@ -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::DeviceInfo> CameraManager::EnumerateDevices()
{
std::scoped_lock lock(m_mutex);
if (!m_device)
return false;
if (m_refCount == 0)
std::vector<DeviceInfo> 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<CapFormatID> 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;
}
}

View file

@ -1,5 +1,7 @@
#pragma once
#include <shared_mutex>
#include <atomic>
#include <mutex>
#include <thread>
#include <openpnp-capture.h>
#include "util/helpers/Singleton.h"
@ -13,19 +15,32 @@ class CameraManager : public Singleton<CameraManager>
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<uint32>::max();
struct DeviceInfo
{
std::string uniqueId;
std::string name;
};
CameraManager();
~CameraManager();
void SetDevice(uint32 deviceNo);
std::vector<DeviceInfo> EnumerateDevices();
void SaveDevice();
bool Open(bool weak);
void Open();
void Close();
void FillNV12Buffer(uint8* nv12Buffer) const;
void FillRGBBuffer(uint8* rgbBuffer) const;
private:
std::optional<CapFormatID> FindCorrectFormat();
void ResetBuffers();
void CaptureWorker();
};
void OpenStream();
void CloseStream();
};

View file

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

View file

@ -499,6 +499,9 @@ struct CemuConfig
ConfigValue<uint16> port{ 26760 };
}dsu_client{};
// camera
ConfigValue<std::string> camera_id;
// debug
ConfigValueBounds<CrashDump> crash_dump{ CrashDump::Disabled };
ConfigValue<uint16> gdb_port{ 1337 };

View file

@ -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$<$<CONFIG:Debug>:Debug>")
@ -139,6 +141,7 @@ target_include_directories(CemuGui PUBLIC ${RAPIDJSON_INCLUDE_DIRS})
target_link_libraries(CemuGui PRIVATE
CemuAudio
CemuCafe
CemuCamera
CemuCommon
CemuComponents
CemuConfig

View file

@ -0,0 +1,98 @@
#include "CameraSettingsWindow.h"
#include "camera/CameraManager.h"
#include <wx/sizer.h>
#include <wx/dcclient.h>
#include <wx/dcbuffer.h>
#include <wx/rawbmp.h>
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();
}

View file

@ -0,0 +1,22 @@
#pragma once
#include <wx/dialog.h>
#include <wx/timer.h>
#include <wx/choice.h>
#include <wx/bmpbuttn.h>
class CameraSettingsWindow : public wxDialog
{
wxChoice* m_cameraChoice;
wxButton* m_refreshButton;
wxWindow* m_imageWindow;
wxBitmap m_imageBitmap;
wxTimer m_imageUpdateTimer;
std::vector<uint8> m_imageBuffer;
public:
explicit CameraSettingsWindow(wxWindow* parent);
void OnSelectCameraChoice(wxCommandEvent&);
void OnRefreshPressed(wxCommandEvent&);
void UpdateImage(const wxTimerEvent&);
void OnClose(wxCloseEvent& event);
};

View file

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