diff --git a/rpcs3/CMakeLists.txt b/rpcs3/CMakeLists.txt index 3556502f7a..386390e6c1 100644 --- a/rpcs3/CMakeLists.txt +++ b/rpcs3/CMakeLists.txt @@ -79,6 +79,7 @@ if (NOT ANDROID) Input/basic_keyboard_handler.cpp Input/basic_mouse_handler.cpp + Input/camera_video_sink.cpp Input/ds3_pad_handler.cpp Input/ds4_pad_handler.cpp Input/dualsense_pad_handler.cpp @@ -96,6 +97,9 @@ if (NOT ANDROID) Input/ps_move_tracker.cpp Input/raw_mouse_config.cpp Input/raw_mouse_handler.cpp + Input/sdl_camera_handler.cpp + Input/sdl_camera_video_sink.cpp + Input/sdl_instance.cpp Input/sdl_pad_handler.cpp Input/skateboard_pad_handler.cpp Input/xinput_pad_handler.cpp diff --git a/rpcs3/Emu/Cell/Modules/cellGem.cpp b/rpcs3/Emu/Cell/Modules/cellGem.cpp index f051b67131..012af40659 100644 --- a/rpcs3/Emu/Cell/Modules/cellGem.cpp +++ b/rpcs3/Emu/Cell/Modules/cellGem.cpp @@ -1331,8 +1331,15 @@ void gem_config_data::operator()() vc = vc_attribute; } - if (g_cfg.io.camera != camera_handler::qt) + switch (g_cfg.io.camera) { +#ifdef HAVE_SDL3 + case camera_handler::sdl: +#endif + case camera_handler::qt: + break; + case camera_handler::fake: + case camera_handler::null: video_conversion_in_progress = false; done(); continue; diff --git a/rpcs3/Emu/Io/camera_config.cpp b/rpcs3/Emu/Io/camera_config.cpp index d7071b05c3..9cc8b5cd6a 100644 --- a/rpcs3/Emu/Io/camera_config.cpp +++ b/rpcs3/Emu/Io/camera_config.cpp @@ -36,32 +36,38 @@ void cfg_camera::save() const } } -cfg_camera::camera_setting cfg_camera::get_camera_setting(const std::string& camera, bool& success) +cfg_camera::camera_setting cfg_camera::get_camera_setting(const std::string& handler, const std::string& camera, bool& success) { - camera_setting setting; - const std::string value = cameras.get_value(camera); + camera_setting setting {}; + const std::string value = cameras.get_value(handler + "-" + camera); success = !value.empty(); if (success) { - setting.from_string(cameras.get_value(camera)); + setting.from_string(value); } return setting; } -void cfg_camera::set_camera_setting(const std::string& camera, const camera_setting& setting) +void cfg_camera::set_camera_setting(const std::string& handler, const std::string& camera, const camera_setting& setting) { + if (handler.empty()) + { + camera_log.error("String '%s' cannot be used as handler key.", handler); + return; + } + if (camera.empty()) { camera_log.error("String '%s' cannot be used as camera key.", camera); return; } - cameras.set_value(camera, setting.to_string()); + cameras.set_value(handler + "-" + camera, setting.to_string()); } std::string cfg_camera::camera_setting::to_string() const { - return fmt::format("%d,%d,%f,%f,%d", width, height, min_fps, max_fps, format); + return fmt::format("%d,%d,%f,%f,%d,%d", width, height, min_fps, max_fps, format, colorspace); } void cfg_camera::camera_setting::from_string(const std::string& text) @@ -106,12 +112,14 @@ void cfg_camera::camera_setting::from_string(const std::string& text) !to_integer(::at32(list, 1), height) || !to_double(::at32(list, 2), min_fps) || !to_double(::at32(list, 3), max_fps) || - !to_integer(::at32(list, 4), format)) + !to_integer(::at32(list, 4), format) || + !to_integer(::at32(list, 4), colorspace)) { width = 0; height = 0; min_fps = 0; max_fps = 0; format = 0; + colorspace = 0; } } diff --git a/rpcs3/Emu/Io/camera_config.h b/rpcs3/Emu/Io/camera_config.h index a918dea458..14196b376c 100644 --- a/rpcs3/Emu/Io/camera_config.h +++ b/rpcs3/Emu/Io/camera_config.h @@ -15,18 +15,19 @@ struct cfg_camera final : cfg::node double min_fps = 0; double max_fps = 0; int format = 0; + int colorspace = 0; - static constexpr u32 member_count = 5; + static constexpr u32 member_count = 6; std::string to_string() const; void from_string(const std::string& text); }; - camera_setting get_camera_setting(const std::string& camera, bool& success); - void set_camera_setting(const std::string& camera, const camera_setting& setting); + camera_setting get_camera_setting(const std::string& handler, const std::string& camera, bool& success); + void set_camera_setting(const std::string& handler, const std::string& camera, const camera_setting& setting); const std::string path; - cfg::map_entry cameras{ this, "Cameras" }; // : ,,,, + cfg::map_entry cameras{ this, "Cameras" }; // : ,,,,, }; extern cfg_camera g_cfg_camera; diff --git a/rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_settings.cpp b/rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_settings.cpp index 119f411a24..430897a8a6 100644 --- a/rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_settings.cpp +++ b/rpcs3/Emu/RSX/Overlays/HomeMenu/overlay_home_menu_settings.cpp @@ -75,9 +75,17 @@ namespace rsx add_checkbox(&g_cfg.io.keep_pads_connected, localized_string_id::HOME_MENU_SETTINGS_INPUT_KEEP_PADS_CONNECTED); add_checkbox(&g_cfg.io.show_move_cursor, localized_string_id::HOME_MENU_SETTINGS_INPUT_SHOW_PS_MOVE_CURSOR); - if (g_cfg.io.camera == camera_handler::qt) + switch (g_cfg.io.camera) { + #ifdef HAVE_SDL3 + case camera_handler::sdl: + #endif + case camera_handler::qt: add_dropdown(&g_cfg.io.camera_flip_option, localized_string_id::HOME_MENU_SETTINGS_INPUT_CAMERA_FLIP); + break; + case camera_handler::fake: + case camera_handler::null: + break; } add_dropdown(&g_cfg.io.pad_mode, localized_string_id::HOME_MENU_SETTINGS_INPUT_PAD_MODE); diff --git a/rpcs3/Emu/system_config.h b/rpcs3/Emu/system_config.h index f68d9d0d28..28f493b4c3 100644 --- a/rpcs3/Emu/system_config.h +++ b/rpcs3/Emu/system_config.h @@ -269,6 +269,7 @@ struct cfg_root : cfg::node cfg::_enum camera_type{ this, "Camera type", fake_camera_type::unknown }; cfg::_enum camera_flip_option{ this, "Camera flip", camera_flip::none, true }; cfg::string camera_id{ this, "Camera ID", "Default", true }; + cfg::string sdl_camera_id{ this, "SDL Camera ID", "Default", true }; cfg::_enum move{ this, "Move", move_handler::null, true }; cfg::_enum buzz{ this, "Buzz emulated controller", buzz_handler::null }; cfg::_enum turntable{this, "Turntable emulated controller", turntable_handler::null}; diff --git a/rpcs3/Emu/system_config_types.cpp b/rpcs3/Emu/system_config_types.cpp index c01692b8a5..d4175eb057 100644 --- a/rpcs3/Emu/system_config_types.cpp +++ b/rpcs3/Emu/system_config_types.cpp @@ -374,6 +374,9 @@ void fmt_class_string::format(std::string& out, u64 arg) case camera_handler::null: return "Null"; case camera_handler::fake: return "Fake"; case camera_handler::qt: return "Qt"; +#ifdef HAVE_SDL3 + case camera_handler::sdl: return "SDL"; +#endif } return unknown; diff --git a/rpcs3/Emu/system_config_types.h b/rpcs3/Emu/system_config_types.h index 9459ba698d..697afa3618 100644 --- a/rpcs3/Emu/system_config_types.h +++ b/rpcs3/Emu/system_config_types.h @@ -116,7 +116,10 @@ enum class camera_handler { null, fake, - qt + qt, +#ifdef HAVE_SDL3 + sdl, +#endif }; enum class camera_flip diff --git a/rpcs3/Input/camera_video_sink.cpp b/rpcs3/Input/camera_video_sink.cpp new file mode 100644 index 0000000000..857b052ac8 --- /dev/null +++ b/rpcs3/Input/camera_video_sink.cpp @@ -0,0 +1,230 @@ +#include "stdafx.h" +#include "camera_video_sink.h" + +#include "Emu/Cell/Modules/cellCamera.h" +#include "Emu/system_config.h" + +LOG_CHANNEL(camera_log, "Camera"); + +camera_video_sink::camera_video_sink(bool front_facing) + : m_front_facing(front_facing) +{ +} + +camera_video_sink::~camera_video_sink() +{ +} + +bool camera_video_sink::present(u32 src_width, u32 src_height, u32 src_pitch, u32 src_bytes_per_pixel, std::function src_line_ptr) +{ + ensure(!!src_line_ptr); + + const u64 new_size = m_bytesize; + image_buffer& image_buffer = m_image_buffer[m_write_index]; + + // Reset buffer if necessary + if (image_buffer.data.size() != new_size) + { + image_buffer.data.clear(); + } + + // Create buffer if necessary + if (image_buffer.data.empty() && new_size > 0) + { + image_buffer.data.resize(new_size); + image_buffer.width = m_width; + image_buffer.height = m_height; + } + + if (!image_buffer.data.empty() && src_width && src_height) + { + // Convert image to proper layout + // TODO: check if pixel format and bytes per pixel match and convert if necessary + // TODO: implement or improve more conversions + + const u32 width = std::min(image_buffer.width, src_width); + const u32 height = std::min(image_buffer.height, src_height); + + switch (m_format) + { + case CELL_CAMERA_RAW8: // The game seems to expect BGGR + { + // Let's use a very simple algorithm to convert the image to raw BGGR + u8* dst = image_buffer.data.data(); + + for (u32 y = 0; y < height; y++) + { + const u8* src = src_line_ptr(y); + const bool is_top_pixel = (y % 2) == 0; + + // Split loops (roughly twice the performance by removing one condition) + if (is_top_pixel) + { + for (u32 x = 0; x < width; x++, dst++, src += 4) + { + const bool is_left_pixel = (x % 2) == 0; + + if (is_left_pixel) + { + *dst = src[2]; // Blue + } + else + { + *dst = src[1]; // Green + } + } + } + else + { + for (u32 x = 0; x < width; x++, dst++, src += 4) + { + const bool is_left_pixel = (x % 2) == 0; + + if (is_left_pixel) + { + *dst = src[1]; // Green + } + else + { + *dst = src[0]; // Red + } + } + } + } + break; + } + //case CELL_CAMERA_YUV422: + case CELL_CAMERA_Y0_U_Y1_V: + case CELL_CAMERA_V_Y1_U_Y0: + { + // Simple RGB to Y0_U_Y1_V conversion from stackoverflow. + constexpr int yuv_bytes_per_pixel = 2; + const int yuv_pitch = image_buffer.width * yuv_bytes_per_pixel; + + const int y0_offset = (m_format == CELL_CAMERA_Y0_U_Y1_V) ? 0 : 3; + const int u_offset = (m_format == CELL_CAMERA_Y0_U_Y1_V) ? 1 : 2; + const int y1_offset = (m_format == CELL_CAMERA_Y0_U_Y1_V) ? 2 : 1; + const int v_offset = (m_format == CELL_CAMERA_Y0_U_Y1_V) ? 3 : 0; + + for (u32 y = 0; y < height; y++) + { + const u8* src = src_line_ptr(y); + u8* yuv_row_ptr = &image_buffer.data[y * yuv_pitch]; + + for (u32 x = 0; x < width - 1; x += 2, src += 8) + { + const f32 r1 = src[0]; + const f32 g1 = src[1]; + const f32 b1 = src[2]; + const f32 r2 = src[4]; + const f32 g2 = src[5]; + const f32 b2 = src[6]; + + const f32 y0 = (0.257f * r1) + (0.504f * g1) + (0.098f * b1) + 16.0f; + const f32 u = -(0.148f * r1) - (0.291f * g1) + (0.439f * b1) + 128.0f; + const f32 v = (0.439f * r1) - (0.368f * g1) - (0.071f * b1) + 128.0f; + const f32 y1 = (0.257f * r2) + (0.504f * g2) + (0.098f * b2) + 16.0f; + + const int yuv_index = x * yuv_bytes_per_pixel; + yuv_row_ptr[yuv_index + y0_offset] = static_cast(std::clamp(y0, 0.0f, 255.0f)); + yuv_row_ptr[yuv_index + u_offset] = static_cast(std::clamp( u, 0.0f, 255.0f)); + yuv_row_ptr[yuv_index + y1_offset] = static_cast(std::clamp(y1, 0.0f, 255.0f)); + yuv_row_ptr[yuv_index + v_offset] = static_cast(std::clamp( v, 0.0f, 255.0f)); + } + } + break; + } + case CELL_CAMERA_JPG: + case CELL_CAMERA_RGBA: + case CELL_CAMERA_RAW10: + case CELL_CAMERA_YUV420: + case CELL_CAMERA_FORMAT_UNKNOWN: + default: + const u32 bytes_per_line = src_bytes_per_pixel * src_width; + if (src_pitch == bytes_per_line) + { + std::memcpy(image_buffer.data.data(), src_line_ptr(0), std::min(image_buffer.data.size(), src_height * bytes_per_line)); + } + else + { + for (u32 y = 0, pos = 0; y < src_height && pos < image_buffer.data.size(); y++, pos += bytes_per_line) + { + std::memcpy(&image_buffer.data[pos], src_line_ptr(y), std::min(image_buffer.data.size() - pos, bytes_per_line)); + } + } + break; + } + } + + camera_log.trace("Wrote image to video surface. index=%d, m_frame_number=%d, width=%d, height=%d, bytesize=%d", + m_write_index, m_frame_number.load(), m_width, m_height, m_bytesize); + + // Toggle write/read index + std::lock_guard lock(m_mutex); + image_buffer.frame_number = m_frame_number++; + m_write_index = read_index(); + + return true; +} + +void camera_video_sink::set_format(s32 format, u32 bytesize) +{ + camera_log.notice("Setting format: format=%d, bytesize=%d", format, bytesize); + + m_format = format; + m_bytesize = bytesize; +} + +void camera_video_sink::set_resolution(u32 width, u32 height) +{ + camera_log.notice("Setting resolution: width=%d, height=%d", width, height); + + m_width = width; + m_height = height; +} + +void camera_video_sink::set_mirrored(bool mirrored) +{ + camera_log.notice("Setting mirrored: mirrored=%d", mirrored); + + m_mirrored = mirrored; +} + +u64 camera_video_sink::frame_number() const +{ + return m_frame_number.load(); +} + +void camera_video_sink::get_image(u8* buf, u64 size, u32& width, u32& height, u64& frame_number, u64& bytes_read) +{ + // Lock read buffer + std::lock_guard lock(m_mutex); + const image_buffer& image_buffer = m_image_buffer[read_index()]; + + width = image_buffer.width; + height = image_buffer.height; + frame_number = image_buffer.frame_number; + + // Copy to out buffer + if (buf && !image_buffer.data.empty()) + { + bytes_read = std::min(image_buffer.data.size(), size); + std::memcpy(buf, image_buffer.data.data(), bytes_read); + + if (image_buffer.data.size() != size) + { + camera_log.error("Buffer size mismatch: in=%d, out=%d. Cropping to incoming size. Please contact a developer.", size, image_buffer.data.size()); + } + } + else + { + bytes_read = 0; + } +} + +u32 camera_video_sink::read_index() const +{ + // The read buffer index cannot be the same as the write index + return (m_write_index + 1u) % ::narrow(m_image_buffer.size()); +} + diff --git a/rpcs3/Input/camera_video_sink.h b/rpcs3/Input/camera_video_sink.h new file mode 100644 index 0000000000..b2a3311199 --- /dev/null +++ b/rpcs3/Input/camera_video_sink.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +class camera_video_sink +{ +public: + camera_video_sink(bool front_facing); + virtual ~camera_video_sink(); + + void set_format(s32 format, u32 bytesize); + void set_resolution(u32 width, u32 height); + void set_mirrored(bool mirrored); + + u64 frame_number() const; + + bool present(u32 src_width, u32 src_height, u32 src_pitch, u32 src_bytes_per_pixel, std::function src_line_ptr); + + void get_image(u8* buf, u64 size, u32& width, u32& height, u64& frame_number, u64& bytes_read); + +protected: + u32 read_index() const; + + bool m_front_facing = false; + bool m_mirrored = false; // Set by cellCamera + s32 m_format = 2; // CELL_CAMERA_RAW8, set by cellCamera + u32 m_bytesize = 0; + u32 m_width = 640; + u32 m_height = 480; + + std::mutex m_mutex; + atomic_t m_frame_number{0}; + u32 m_write_index{0}; + + struct image_buffer + { + u64 frame_number = 0; + u32 width = 0; + u32 height = 0; + std::vector data; + }; + std::array m_image_buffer; +}; diff --git a/rpcs3/Input/sdl_camera_handler.cpp b/rpcs3/Input/sdl_camera_handler.cpp new file mode 100644 index 0000000000..d2d30bf1a8 --- /dev/null +++ b/rpcs3/Input/sdl_camera_handler.cpp @@ -0,0 +1,462 @@ +#ifdef HAVE_SDL3 + +#include "stdafx.h" +#include "sdl_camera_handler.h" +#include "sdl_camera_video_sink.h" +#include "sdl_instance.h" +#include "Emu/system_config.h" +#include "Emu/System.h" +#include "Emu/Io/camera_config.h" + +LOG_CHANNEL(camera_log, "Camera"); + +template <> +void fmt_class_string::format(std::string& out, u64 arg) +{ + const SDL_CameraSpec& spec = get_object(arg); + out += fmt::format("format=0x%x, colorspace=0x%x, width=%d, height=%d, framerate_numerator=%d, framerate_denominator=%d, fps=%f", + static_cast(spec.format), static_cast(spec.colorspace), spec.width, spec.height, + spec.framerate_numerator, spec.framerate_denominator, spec.framerate_numerator / static_cast(spec.framerate_denominator)); +} + +std::vector sdl_camera_handler::get_drivers() +{ + std::vector drivers; + + if (const int num_drivers = SDL_GetNumCameraDrivers(); num_drivers > 0) + { + for (int i = 0; i < num_drivers; i++) + { + if (const char* driver = SDL_GetCameraDriver(i)) + { + camera_log.notice("Found driver: %s", driver); + drivers.push_back(driver); + continue; + } + + camera_log.error("Failed to get driver %d. SDL Error: %s", i, SDL_GetError()); + } + } + else + { + camera_log.error("No SDL camera drivers found"); + } + + return drivers; +} + +std::map sdl_camera_handler::get_cameras() +{ + int camera_count = 0; + if (SDL_CameraID* cameras = SDL_GetCameras(&camera_count)) + { + std::map camera_map; + + for (int i = 0; i < camera_count && cameras[i]; i++) + { + if (const char* name = SDL_GetCameraName(cameras[i])) + { + camera_log.notice("Found camera: name=%s", name); + camera_map[cameras[i]] = name; + continue; + } + + camera_log.error("Found camera (Failed to get name. SDL Error: %s", SDL_GetError()); + } + + SDL_free(cameras); + return camera_map; + } + + camera_log.error("Could not get cameras! SDL Error: %s", SDL_GetError()); + return {}; +} + +sdl_camera_handler::sdl_camera_handler() : camera_handler_base() +{ + if (!g_cfg_camera.load()) + { + camera_log.notice("Could not load camera config. Using defaults."); + } + + if (!sdl_instance::get_instance().initialize()) + { + camera_log.error("Could not initialize SDL"); + return; + } + + // List available camera drivers + sdl_camera_handler::get_drivers(); + + // List available cameras + sdl_camera_handler::get_cameras(); +} + +sdl_camera_handler::~sdl_camera_handler() +{ + Emu.BlockingCallFromMainThread([&]() + { + close_camera(); + }); +} + +void sdl_camera_handler::reset() +{ + m_video_sink.reset(); + + if (m_camera) + { + SDL_CloseCamera(m_camera); + m_camera = nullptr; + } +} + +void sdl_camera_handler::open_camera() +{ + camera_log.notice("Loading camera"); + + if (const std::string camera_id = g_cfg.io.sdl_camera_id.to_string(); + m_camera_id != camera_id) + { + camera_log.notice("Switching camera from %s to %s", m_camera_id, camera_id); + camera_log.notice("Stopping old camera..."); + if (m_camera) + { + set_expected_state(camera_handler_state::open); + reset(); + } + m_camera_id = camera_id; + } + + // List available cameras + int camera_count = 0; + SDL_CameraID* cameras = SDL_GetCameras(&camera_count); + + if (!cameras) + { + camera_log.error("Could not get cameras! SDL Error: %s", SDL_GetError()); + set_state(camera_handler_state::closed); + return; + } + + if (camera_count <= 0) + { + camera_log.error("No cameras found"); + set_state(camera_handler_state::closed); + SDL_free(cameras); + return; + } + + m_sdl_camera_id = 0; + + if (m_camera_id == g_cfg.io.sdl_camera_id.def) + { + m_sdl_camera_id = cameras[0]; + } + else if (!m_camera_id.empty()) + { + for (int i = 0; i < camera_count && cameras[i]; i++) + { + if (const char* name = SDL_GetCameraName(cameras[i])) + { + if (m_camera_id == name) + { + m_sdl_camera_id = cameras[i]; + break; + } + } + } + } + + SDL_free(cameras); + + if (!m_sdl_camera_id) + { + camera_log.error("Camera %s not found", m_camera_id); + set_state(camera_handler_state::closed); + return; + } + + std::string camera_id; + + if (const char* name = SDL_GetCameraName(m_sdl_camera_id)) + { + camera_log.notice("Using camera: name=%s", name); + camera_id = name; + } + + SDL_CameraSpec used_spec + { + .format = SDL_PixelFormat::SDL_PIXELFORMAT_RGBA32, + .colorspace = SDL_Colorspace::SDL_COLORSPACE_RGB_DEFAULT, + .width = static_cast(m_width), + .height = static_cast(m_height), + .framerate_numerator = 30, + .framerate_denominator = 1 + }; + + int num_formats = 0; + if (SDL_CameraSpec** specs = SDL_GetCameraSupportedFormats(m_sdl_camera_id, &num_formats)) + { + if (num_formats <= 0) + { + camera_log.error("No SDL camera specs found"); + } + else + { + // Load selected settings from config file + bool success = false; + cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(fmt::format("%s", camera_handler::sdl), camera_id, success); + + if (success) + { + camera_log.notice("Found config entry for camera \"%s\" (m_camera_id='%s')", camera_id, m_camera_id); + + // List all available settings and choose the proper value if possible. + constexpr double epsilon = 0.001; + success = false; + + for (int i = 0; i < num_formats; i++) + { + if (!specs[i]) continue; + + const SDL_CameraSpec& spec = *specs[i]; + const f64 fps = spec.framerate_numerator / static_cast(spec.framerate_denominator); + + if (spec.width == cfg_setting.width && + spec.height == cfg_setting.height && + fps >= (cfg_setting.min_fps - epsilon) && + fps <= (cfg_setting.min_fps + epsilon) && + fps >= (cfg_setting.max_fps - epsilon) && + fps <= (cfg_setting.max_fps + epsilon) && + spec.format == static_cast(cfg_setting.format) && + spec.colorspace == static_cast(cfg_setting.colorspace)) + { + // Apply settings. + camera_log.notice("Setting camera spec: %s", spec); + + // TODO: SDL converts the image for us. We would have to do this manually if we want to use other formats. + //used_spec = spec; + used_spec.width = spec.width; + used_spec.height = spec.height; + used_spec.framerate_numerator = spec.framerate_numerator; + used_spec.framerate_denominator = spec.framerate_denominator; + success = true; + break; + } + } + + if (!success) + { + camera_log.warning("No matching camera setting available for the camera config: max_fps=%f, width=%d, height=%d, format=%d, colorspace=%d", + cfg_setting.max_fps, cfg_setting.width, cfg_setting.height, cfg_setting.format, cfg_setting.colorspace); + } + } + + if (!success) + { + camera_log.notice("Using default camera spec: %s", used_spec); + } + } + SDL_free(specs); + } + else + { + camera_log.error("No SDL camera specs found. SDL Error: %s", SDL_GetError()); + } + + reset(); + + camera_log.notice("Requesting camera spec: %s", used_spec); + + m_camera = SDL_OpenCamera(m_sdl_camera_id, &used_spec); + + if (!m_camera) + { + if (!m_camera_id.empty()) camera_log.notice("Camera disabled"); + else camera_log.error("No camera found"); + set_state(camera_handler_state::closed); + return; + } + + if (const char* driver = SDL_GetCurrentCameraDriver()) + { + camera_log.notice("Using driver: %s", driver); + } + + if (SDL_CameraSpec spec {}; SDL_GetCameraFormat(m_camera, &spec)) + { + camera_log.notice("Using camera spec: %s", spec); + } + else + { + camera_log.error("Could not get camera spec. SDL Error: %s", SDL_GetError()); + } + + const SDL_CameraPosition position = SDL_GetCameraPosition(m_sdl_camera_id); + const bool front_facing = position == SDL_CameraPosition::SDL_CAMERA_POSITION_FRONT_FACING; + + // TODO: this doesn't seem to have any properties at the moment + // const SDL_PropertiesID property_id = SDL_GetCameraProperties(m_camera); + // SDL_HasProperty(property_id, ...); + + m_video_sink = std::make_unique(front_facing, m_camera); + m_video_sink->set_resolution(m_width, m_height); + m_video_sink->set_format(m_format, m_bytesize); + m_video_sink->set_mirrored(m_mirrored); + + set_state(camera_handler_state::open); +} + +void sdl_camera_handler::close_camera() +{ + camera_log.notice("Unloading camera"); + + if (!m_camera) + { + if (m_camera_id.empty()) camera_log.notice("Camera disabled"); + else camera_log.error("No camera found"); + set_state(camera_handler_state::closed); + return; + } + + // Unload/close camera + reset(); + + set_state(camera_handler_state::closed); +} + +void sdl_camera_handler::start_camera() +{ + camera_log.notice("Starting camera"); + + if (!m_camera) + { + if (m_camera_id.empty()) camera_log.notice("Camera disabled"); + else camera_log.error("No camera found"); + set_state(camera_handler_state::closed); + return; + } + + const int camera_permission = SDL_GetCameraPermissionState(m_camera); + switch (camera_permission) + { + case -1: // Denied + camera_log.error("Camera permission denied"); + set_state(camera_handler_state::closed); + reset(); + return; + case 0: // Pending + // TODO: try to get permission + break; + case 1: // Approved + break; + } + + // Start camera. We will start receiving frames now. + set_state(camera_handler_state::running); +} + +void sdl_camera_handler::stop_camera() +{ + camera_log.notice("Stopping camera"); + + if (!m_camera) + { + if (m_camera_id.empty()) camera_log.notice("Camera disabled"); + else camera_log.error("No camera found"); + set_state(camera_handler_state::closed); + return; + } + + // Stop camera. The camera will still be drawing power. + set_expected_state(camera_handler_state::open); +} + +void sdl_camera_handler::set_format(s32 format, u32 bytesize) +{ + m_format = format; + m_bytesize = bytesize; + + if (m_video_sink) + { + m_video_sink->set_format(m_format, m_bytesize); + } +} + +void sdl_camera_handler::set_frame_rate(u32 frame_rate) +{ + m_frame_rate = frame_rate; +} + +void sdl_camera_handler::set_resolution(u32 width, u32 height) +{ + m_width = width; + m_height = height; + + if (m_video_sink) + { + m_video_sink->set_resolution(m_width, m_height); + } +} + +void sdl_camera_handler::set_mirrored(bool mirrored) +{ + m_mirrored = mirrored; + + if (m_video_sink) + { + m_video_sink->set_mirrored(m_mirrored); + } +} + +u64 sdl_camera_handler::frame_number() const +{ + return m_video_sink ? m_video_sink->frame_number() : 0; +} + +camera_handler_base::camera_handler_state sdl_camera_handler::get_image(u8* buf, u64 size, u32& width, u32& height, u64& frame_number, u64& bytes_read) +{ + width = 0; + height = 0; + frame_number = 0; + bytes_read = 0; + + if (const std::string camera_id = g_cfg.io.sdl_camera_id.to_string(); + m_camera_id != camera_id) + { + camera_log.notice("Switching cameras"); + set_state(camera_handler_state::closed); + return camera_handler_state::closed; + } + + if (m_camera_id.empty()) + { + camera_log.notice("Camera disabled"); + set_state(camera_handler_state::closed); + return camera_handler_state::closed; + } + + if (!m_camera || !m_video_sink) + { + camera_log.fatal("Error: camera invalid"); + set_state(camera_handler_state::closed); + return camera_handler_state::closed; + } + + // Backup current state. State may change through events. + const camera_handler_state current_state = get_state(); + + if (current_state == camera_handler_state::running) + { + m_video_sink->get_image(buf, size, width, height, frame_number, bytes_read); + } + else + { + camera_log.error("Camera not running (m_state=%d)", static_cast(current_state)); + } + + return current_state; +} + +#endif diff --git a/rpcs3/Input/sdl_camera_handler.h b/rpcs3/Input/sdl_camera_handler.h new file mode 100644 index 0000000000..1e177ff32e --- /dev/null +++ b/rpcs3/Input/sdl_camera_handler.h @@ -0,0 +1,49 @@ +#pragma once + +#ifdef HAVE_SDL3 + +#include "Emu/Io/camera_handler_base.h" + +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "SDL3/SDL.h" +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif + +#include + +class sdl_camera_video_sink; + +class sdl_camera_handler : public camera_handler_base +{ +public: + sdl_camera_handler(); + virtual ~sdl_camera_handler(); + + void open_camera() override; + void close_camera() override; + void start_camera() override; + void stop_camera() override; + void set_format(s32 format, u32 bytesize) override; + void set_frame_rate(u32 frame_rate) override; + void set_resolution(u32 width, u32 height) override; + void set_mirrored(bool mirrored) override; + u64 frame_number() const override; + camera_handler_state get_image(u8* buf, u64 size, u32& width, u32& height, u64& frame_number, u64& bytes_read) override; + + static std::vector get_drivers(); + static std::map get_cameras(); + +private: + void reset(); + + std::string m_camera_id; + SDL_CameraID m_sdl_camera_id = 0; + SDL_Camera* m_camera = nullptr; + std::unique_ptr m_video_sink; +}; + +#endif diff --git a/rpcs3/Input/sdl_camera_video_sink.cpp b/rpcs3/Input/sdl_camera_video_sink.cpp new file mode 100644 index 0000000000..346e0a6874 --- /dev/null +++ b/rpcs3/Input/sdl_camera_video_sink.cpp @@ -0,0 +1,168 @@ +#ifdef HAVE_SDL3 + +#include "stdafx.h" +#include "sdl_camera_video_sink.h" +#include "Utilities/Thread.h" +#include "Emu/system_config.h" + +LOG_CHANNEL(camera_log, "Camera"); + +sdl_camera_video_sink::sdl_camera_video_sink(bool front_facing, SDL_Camera* camera) + : camera_video_sink(front_facing), m_camera(camera) +{ + ensure(m_camera); + + m_thread = std::make_unique(&sdl_camera_video_sink::run, this); +} + +sdl_camera_video_sink::~sdl_camera_video_sink() +{ + m_terminate = true; + + if (m_thread && m_thread->joinable()) + { + m_thread->join(); + m_thread.reset(); + } +} + +void sdl_camera_video_sink::present(SDL_Surface* frame) +{ + const int bytes_per_pixel = SDL_BYTESPERPIXEL(frame->format); + const u32 src_width_in_bytes = std::max(0, frame->w * bytes_per_pixel); + const u32 dst_width_in_bytes = std::max(0, m_width * bytes_per_pixel); + const u8* pixels = reinterpret_cast(frame->pixels); + + bool use_buffer = false; + + // Scale image if necessary + const bool scale_image = m_width > 0 && m_height > 0 && m_width != static_cast(frame->w) && m_height != static_cast(frame->h); + + // Determine image flip + const camera_flip flip_setting = g_cfg.io.camera_flip_option; + + bool flip_horizontally = m_front_facing; // Front facing cameras are flipped already + if (flip_setting == camera_flip::horizontal || flip_setting == camera_flip::both) + { + flip_horizontally = !flip_horizontally; + } + if (m_mirrored) // Set by the game + { + flip_horizontally = !flip_horizontally; + } + + bool flip_vertically = false; + if (flip_setting == camera_flip::vertical || flip_setting == camera_flip::both) + { + flip_vertically = !flip_vertically; + } + + // Flip image if necessary + if (flip_horizontally || flip_vertically || scale_image) + { + m_buffer.resize(m_height * dst_width_in_bytes); + use_buffer = true; + + if (m_width > 0 && m_height > 0 && frame->w > 0 && frame->h > 0) + { + const f32 scale_x = frame->w / static_cast(m_width); + const f32 scale_y = frame->h / static_cast(m_height); + + if (flip_horizontally && flip_vertically) + { + for (u32 y = 0; y < m_height; y++) + { + const u32 src_y = frame->h - static_cast(scale_y * y) - 1; + const u8* src = pixels + src_y * src_width_in_bytes; + u8* dst = &m_buffer[y * dst_width_in_bytes]; + + for (u32 x = 0; x < m_width; x++) + { + const u32 src_x = frame->w - static_cast(scale_x * x) - 1; + std::memcpy(dst + x * bytes_per_pixel, src + src_x * bytes_per_pixel, bytes_per_pixel); + } + } + } + else if (flip_horizontally) + { + for (u32 y = 0; y < m_height; y++) + { + const u32 src_y = static_cast(scale_y * y); + const u8* src = pixels + src_y * src_width_in_bytes; + u8* dst = &m_buffer[y * dst_width_in_bytes]; + + for (u32 x = 0; x < m_width; x++) + { + const u32 src_x = frame->w - static_cast(scale_x * x) - 1; + std::memcpy(dst + x * bytes_per_pixel, src + src_x * bytes_per_pixel, bytes_per_pixel); + } + } + } + else if (flip_vertically) + { + for (u32 y = 0; y < m_height; y++) + { + const u32 src_y = frame->h - static_cast(scale_y * y) - 1; + const u8* src = pixels + src_y * src_width_in_bytes; + u8* dst = &m_buffer[y * dst_width_in_bytes]; + + for (u32 x = 0; x < m_width; x++) + { + const u32 src_x = static_cast(scale_x * x); + std::memcpy(dst + x * bytes_per_pixel, src + src_x * bytes_per_pixel, bytes_per_pixel); + } + } + } + else + { + for (u32 y = 0; y < m_height; y++) + { + const u32 src_y = static_cast(scale_y * y); + const u8* src = pixels + src_y * src_width_in_bytes; + u8* dst = &m_buffer[y * dst_width_in_bytes]; + + for (u32 x = 0; x < m_width; x++) + { + const u32 src_x = static_cast(scale_x * x); + std::memcpy(dst + x * bytes_per_pixel, src + src_x * bytes_per_pixel, bytes_per_pixel); + } + } + } + } + } + + if (use_buffer) + { + camera_video_sink::present(m_width, m_height, dst_width_in_bytes, bytes_per_pixel, [src = m_buffer.data(), dst_width_in_bytes](u32 y){ return src + y * dst_width_in_bytes; }); + } + else + { + camera_video_sink::present(frame->w, frame->h, frame->pitch, bytes_per_pixel, [pixels, pitch = frame->pitch](u32 y){ return pixels + y * pitch; }); + } +} + +void sdl_camera_video_sink::run() +{ + thread_base::set_name("SDL Capture Thread"); + + camera_log.notice("SDL Capture Thread started"); + + while (!m_terminate) + { + // Copy latest image into out buffer. + u64 timestamp_ns = 0; + SDL_Surface* frame = SDL_AcquireCameraFrame(m_camera, ×tamp_ns); + if (!frame) + { + // No new frame + std::this_thread::sleep_for(100us); + continue; + } + + present(frame); + + SDL_ReleaseCameraFrame(m_camera, frame); + } +} + +#endif diff --git a/rpcs3/Input/sdl_camera_video_sink.h b/rpcs3/Input/sdl_camera_video_sink.h new file mode 100644 index 0000000000..010834ff58 --- /dev/null +++ b/rpcs3/Input/sdl_camera_video_sink.h @@ -0,0 +1,34 @@ +#pragma once + +#ifdef HAVE_SDL3 + +#include "Input/camera_video_sink.h" + +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "SDL3/SDL.h" +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif + +#include + +class sdl_camera_video_sink final : public camera_video_sink +{ +public: + sdl_camera_video_sink(bool front_facing, SDL_Camera* camera); + virtual ~sdl_camera_video_sink(); + +private: + void present(SDL_Surface* frame); + void run(); + + std::vector m_buffer; + atomic_t m_terminate = false; + SDL_Camera* m_camera = nullptr; + std::unique_ptr m_thread; +}; + +#endif diff --git a/rpcs3/Input/sdl_instance.cpp b/rpcs3/Input/sdl_instance.cpp new file mode 100644 index 0000000000..7568fbf4c1 --- /dev/null +++ b/rpcs3/Input/sdl_instance.cpp @@ -0,0 +1,137 @@ +#ifdef HAVE_SDL3 + +#include "stdafx.h" +#include "sdl_instance.h" +#include "Emu/System.h" + +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "SDL3/SDL.h" +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif + +LOG_CHANNEL(sdl_log, "SDL"); + +sdl_instance::~sdl_instance() +{ + // Only quit SDL once on exit. SDL uses a global state internally... + if (m_initialized) + { + sdl_log.notice("Quitting SDL ..."); + SDL_Quit(); + } +} + +bool sdl_instance::initialize() +{ + std::lock_guard lock(mtx); + + if (m_initialized) + { + return true; + } + + bool instance_success = false; + + Emu.BlockingCallFromMainThread([this, &instance_success]() + { + instance_success = initialize_impl(); + }); + + return instance_success; +} + +bool sdl_instance::initialize_impl() +{ + // Only init SDL once. SDL uses a global state internally... + if (m_initialized) + { + return true; + } + + sdl_log.notice("Initializing SDL ..."); + + // Set non-dynamic hints before SDL_Init + if (!SDL_SetHint(SDL_HINT_JOYSTICK_THREAD, "1")) + { + sdl_log.error("Could not set SDL_HINT_JOYSTICK_THREAD: %s", SDL_GetError()); + } + + if (!SDL_Init(SDL_INIT_GAMEPAD | SDL_INIT_CAMERA)) + { + sdl_log.error("Could not initialize! SDL Error: %s", SDL_GetError()); + return false; + } + + SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE); + SDL_SetLogOutputFunction([](void*, int category, SDL_LogPriority priority, const char* message) + { + std::string category_name; + switch (category) + { + case SDL_LOG_CATEGORY_APPLICATION: + category_name = "app"; + break; + case SDL_LOG_CATEGORY_ERROR: + category_name = "error"; + break; + case SDL_LOG_CATEGORY_ASSERT: + category_name = "assert"; + break; + case SDL_LOG_CATEGORY_SYSTEM: + category_name = "system"; + break; + case SDL_LOG_CATEGORY_AUDIO: + category_name = "audio"; + break; + case SDL_LOG_CATEGORY_VIDEO: + category_name = "video"; + break; + case SDL_LOG_CATEGORY_RENDER: + category_name = "render"; + break; + case SDL_LOG_CATEGORY_INPUT: + category_name = "input"; + break; + case SDL_LOG_CATEGORY_TEST: + category_name = "test"; + break; + case SDL_LOG_CATEGORY_GPU: + category_name = "gpu"; + break; + default: + category_name = fmt::format("unknown(%d)", category); + break; + } + + switch (priority) + { + case SDL_LOG_PRIORITY_VERBOSE: + case SDL_LOG_PRIORITY_DEBUG: + sdl_log.trace("%s: %s", category_name, message); + break; + case SDL_LOG_PRIORITY_INFO: + sdl_log.notice("%s: %s", category_name, message); + break; + case SDL_LOG_PRIORITY_WARN: + sdl_log.warning("%s: %s", category_name, message); + break; + case SDL_LOG_PRIORITY_ERROR: + sdl_log.error("%s: %s", category_name, message); + break; + case SDL_LOG_PRIORITY_CRITICAL: + sdl_log.error("%s: %s", category_name, message); + break; + default: + break; + } + }, nullptr); + + m_initialized = true; + return true; +} + +#endif diff --git a/rpcs3/Input/sdl_instance.h b/rpcs3/Input/sdl_instance.h new file mode 100644 index 0000000000..8dae5f9850 --- /dev/null +++ b/rpcs3/Input/sdl_instance.h @@ -0,0 +1,28 @@ +#pragma once + +#ifdef HAVE_SDL3 + +#include + +struct sdl_instance +{ +public: + sdl_instance() = default; + virtual ~sdl_instance(); + + static sdl_instance& get_instance() + { + static sdl_instance instance {}; + return instance; + } + + bool initialize(); + +private: + bool initialize_impl(); + + bool m_initialized = false; + std::mutex mtx; +}; + +#endif diff --git a/rpcs3/Input/sdl_pad_handler.cpp b/rpcs3/Input/sdl_pad_handler.cpp index 38bd6bc1bd..78166d88fe 100644 --- a/rpcs3/Input/sdl_pad_handler.cpp +++ b/rpcs3/Input/sdl_pad_handler.cpp @@ -2,6 +2,7 @@ #include "stdafx.h" #include "sdl_pad_handler.h" +#include "sdl_instance.h" #include "Emu/system_utils.hpp" #include "Emu/system_config.h" #include "Emu/System.h" @@ -10,117 +11,6 @@ LOG_CHANNEL(sdl_log, "SDL"); -struct sdl_instance -{ -public: - sdl_instance() = default; - ~sdl_instance() - { - // Only quit SDL once on exit. SDL uses a global state internally... - if (m_initialized) - { - sdl_log.notice("Quitting SDL ..."); - SDL_Quit(); - } - } - - static sdl_instance& get_instance() - { - static sdl_instance instance {}; - return instance; - } - - bool initialize() - { - // Only init SDL once. SDL uses a global state internally... - if (m_initialized) - { - return true; - } - - sdl_log.notice("Initializing SDL ..."); - - // Set non-dynamic hints before SDL_Init - if (!SDL_SetHint(SDL_HINT_JOYSTICK_THREAD, "1")) - { - sdl_log.error("Could not set SDL_HINT_JOYSTICK_THREAD: %s", SDL_GetError()); - } - - if (!SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD)) - { - sdl_log.error("Could not initialize! SDL Error: %s", SDL_GetError()); - return false; - } - - SDL_SetLogPriorities(SDL_LOG_PRIORITY_VERBOSE); - SDL_SetLogOutputFunction([](void*, int category, SDL_LogPriority priority, const char* message) - { - std::string category_name; - switch (category) - { - case SDL_LOG_CATEGORY_APPLICATION: - category_name = "app"; - break; - case SDL_LOG_CATEGORY_ERROR: - category_name = "error"; - break; - case SDL_LOG_CATEGORY_ASSERT: - category_name = "assert"; - break; - case SDL_LOG_CATEGORY_SYSTEM: - category_name = "system"; - break; - case SDL_LOG_CATEGORY_AUDIO: - category_name = "audio"; - break; - case SDL_LOG_CATEGORY_VIDEO: - category_name = "video"; - break; - case SDL_LOG_CATEGORY_RENDER: - category_name = "render"; - break; - case SDL_LOG_CATEGORY_INPUT: - category_name = "input"; - break; - case SDL_LOG_CATEGORY_TEST: - category_name = "test"; - break; - default: - category_name = fmt::format("unknown(%d)", category); - break; - } - - switch (priority) - { - case SDL_LOG_PRIORITY_VERBOSE: - case SDL_LOG_PRIORITY_DEBUG: - sdl_log.trace("%s: %s", category_name, message); - break; - case SDL_LOG_PRIORITY_INFO: - sdl_log.notice("%s: %s", category_name, message); - break; - case SDL_LOG_PRIORITY_WARN: - sdl_log.warning("%s: %s", category_name, message); - break; - case SDL_LOG_PRIORITY_ERROR: - sdl_log.error("%s: %s", category_name, message); - break; - case SDL_LOG_PRIORITY_CRITICAL: - sdl_log.error("%s: %s", category_name, message); - break; - default: - break; - } - }, nullptr); - - m_initialized = true; - return true; - } - -private: - bool m_initialized = false; -}; - sdl_pad_handler::sdl_pad_handler() : PadHandlerBase(pad_handler::sdl) { button_list = @@ -266,14 +156,7 @@ bool sdl_pad_handler::Init() if (m_is_init) return true; - bool instance_success; - - Emu.BlockingCallFromMainThread([&instance_success]() - { - instance_success = sdl_instance::get_instance().initialize(); - }); - - if (!instance_success) + if (!sdl_instance::get_instance().initialize()) return false; if (g_cfg.io.load_sdl_mappings) diff --git a/rpcs3/headless_application.cpp b/rpcs3/headless_application.cpp index ee7cd42cee..8c9288299a 100644 --- a/rpcs3/headless_application.cpp +++ b/rpcs3/headless_application.cpp @@ -98,6 +98,9 @@ void headless_application::InitializeCallbacks() return std::make_shared(); } case camera_handler::qt: +#ifdef HAVE_SDL3 + case camera_handler::sdl: +#endif { fmt::throw_exception("Headless mode can not be used with this camera handler. Current handler: %s", g_cfg.io.camera.get()); } diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index ce0b4fd6c7..adedd71b27 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -175,6 +175,7 @@ + @@ -185,6 +186,9 @@ + + + @@ -890,6 +894,7 @@ + @@ -1004,6 +1009,9 @@ + + + diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index a2e4afd1a6..aa8dae7683 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -1176,6 +1176,18 @@ Gui\game list + + Io\camera + + + Io\SDL + + + Io\camera + + + Io\camera + @@ -1385,6 +1397,18 @@ Gui\game list + + Io\camera + + + Io\SDL + + + Io\camera + + + Io\camera + diff --git a/rpcs3/rpcs3qt/camera_settings_dialog.cpp b/rpcs3/rpcs3qt/camera_settings_dialog.cpp index 0cc5843595..e926426324 100644 --- a/rpcs3/rpcs3qt/camera_settings_dialog.cpp +++ b/rpcs3/rpcs3qt/camera_settings_dialog.cpp @@ -3,11 +3,18 @@ #include "ui_camera_settings_dialog.h" #include "permissions.h" #include "Emu/Io/camera_config.h" +#include "Emu/System.h" +#include "Input/sdl_instance.h" #include #include #include #include +#include + +#ifdef HAVE_SDL3 +#include "Input/sdl_camera_handler.h" +#endif LOG_CHANNEL(camera_log, "Camera"); @@ -53,6 +60,80 @@ void fmt_class_string::format(std::string& out, }); } +#ifdef HAVE_SDL3 +static QString sdl_pixelformat_to_string(SDL_PixelFormat format) +{ + switch (format) + { + case SDL_PixelFormat::SDL_PIXELFORMAT_UNKNOWN: return "UNKNOWN"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX1LSB: return "INDEX1LSB"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX1MSB: return "INDEX1MSB"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX2LSB: return "INDEX2LSB"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX2MSB: return "INDEX2MSB"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX4LSB: return "INDEX4LSB"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX4MSB: return "INDEX4MSB"; + case SDL_PixelFormat::SDL_PIXELFORMAT_INDEX8: return "INDEX8"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGB332: return "RGB332"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XRGB4444: return "XRGB4444"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XBGR4444: return "XBGR4444"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XRGB1555: return "XRGB1555"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XBGR1555: return "XBGR1555"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB4444: return "ARGB4444"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBA4444: return "RGBA4444"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR4444: return "ABGR4444"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRA4444: return "BGRA4444"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB1555: return "ARGB1555"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBA5551: return "RGBA5551"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR1555: return "ABGR1555"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRA5551: return "BGRA5551"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGB565: return "RGB565"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGR565: return "BGR565"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGB24: return "RGB24"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGR24: return "BGR24"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XRGB8888: return "XRGB8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBX8888: return "RGBX8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XBGR8888: return "XBGR8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRX8888: return "BGRX8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB8888: return "ARGB8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBA8888: return "RGBA8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR8888: return "ABGR8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRA8888: return "BGRA8888"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XRGB2101010: return "XRGB2101010"; + case SDL_PixelFormat::SDL_PIXELFORMAT_XBGR2101010: return "XBGR2101010"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB2101010: return "ARGB2101010"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR2101010: return "ABGR2101010"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGB48: return "RGB48"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGR48: return "BGR48"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBA64: return "RGBA64"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB64: return "ARGB64"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRA64: return "BGRA64"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR64: return "ABGR64"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGB48_FLOAT: return "RGB48_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGR48_FLOAT: return "BGR48_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBA64_FLOAT: return "RGBA64_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB64_FLOAT: return "ARGB64_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRA64_FLOAT: return "BGRA64_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR64_FLOAT: return "ABGR64_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGB96_FLOAT: return "RGB96_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGR96_FLOAT: return "BGR96_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_RGBA128_FLOAT: return "RGBA128_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ARGB128_FLOAT: return "ARGB128_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_BGRA128_FLOAT: return "BGRA128_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_ABGR128_FLOAT: return "ABGR128_FLOAT"; + case SDL_PixelFormat::SDL_PIXELFORMAT_YV12: return "YV12"; + case SDL_PixelFormat::SDL_PIXELFORMAT_IYUV: return "IYUV"; + case SDL_PixelFormat::SDL_PIXELFORMAT_YUY2: return "YUY2"; + case SDL_PixelFormat::SDL_PIXELFORMAT_UYVY: return "UYVY"; + case SDL_PixelFormat::SDL_PIXELFORMAT_YVYU: return "YVYU"; + case SDL_PixelFormat::SDL_PIXELFORMAT_NV12: return "NV12"; + case SDL_PixelFormat::SDL_PIXELFORMAT_NV21: return "NV21"; + case SDL_PixelFormat::SDL_PIXELFORMAT_P010: return "P010"; + case SDL_PixelFormat::SDL_PIXELFORMAT_EXTERNAL_OES: return "EXTERNAL_OES"; + default: return QObject::tr("Unknown: %0").arg(static_cast(format)); + } +} +#endif + Q_DECLARE_METATYPE(QCameraDevice); camera_settings_dialog::camera_settings_dialog(QWidget* parent) @@ -61,15 +142,16 @@ camera_settings_dialog::camera_settings_dialog(QWidget* parent) { ui->setupUi(this); + setAttribute(Qt::WA_DeleteOnClose); + load_config(); - for (const QCameraDevice& camera_info : QMediaDevices::videoInputs()) - { - if (camera_info.isNull()) continue; - ui->combo_camera->addItem(camera_info.description(), QVariant::fromValue(camera_info)); - camera_log.notice("Found camera: '%s'", camera_info.description()); - } + ui->combo_handlers->addItem("Qt", QVariant::fromValue(static_cast(camera_handler::qt))); +#ifdef HAVE_SDL3 + ui->combo_handlers->addItem("SDL", QVariant::fromValue(static_cast(camera_handler::sdl))); +#endif + connect(ui->combo_handlers, QOverload::of(&QComboBox::currentIndexChanged), this, &camera_settings_dialog::handle_handler_change); connect(ui->combo_camera, QOverload::of(&QComboBox::currentIndexChanged), this, &camera_settings_dialog::handle_camera_change); connect(ui->combo_settings, QOverload::of(&QComboBox::currentIndexChanged), this, &camera_settings_dialog::handle_settings_change); connect(ui->buttonBox, &QDialogButtonBox::clicked, [this](QAbstractButton* button) @@ -85,33 +167,182 @@ camera_settings_dialog::camera_settings_dialog(QWidget* parent) } }); - if (ui->combo_camera->count() == 0) - { - ui->combo_camera->setEnabled(false); - ui->combo_settings->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(false); - ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false); - } - else + ui->combo_handlers->setCurrentIndex(0); +} + +camera_settings_dialog::~camera_settings_dialog() +{ + reset_cameras(); +} + +void camera_settings_dialog::enable_combos() +{ + reset_cameras(); + + const bool is_enabled = ui->combo_camera->count() > 0; + + ui->combo_camera->setEnabled(is_enabled); + ui->combo_settings->setEnabled(is_enabled); + ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(is_enabled); + ui->buttonBox->button(QDialogButtonBox::Apply)->setEnabled(is_enabled); + + if (is_enabled) { // TODO: show camera ID somewhere ui->combo_camera->setCurrentIndex(0); } } -camera_settings_dialog::~camera_settings_dialog() +void camera_settings_dialog::reset_cameras() { +#ifdef HAVE_SDL3 + if (m_sdl_thread) + { + auto& thread = *m_sdl_thread; + thread = thread_state::aborting; + thread(); + m_sdl_thread.reset(); + } + + if (m_sdl_camera) + { + SDL_CloseCamera(m_sdl_camera); + m_sdl_camera = nullptr; + } + + m_video_frame_input.reset(); +#endif + + m_camera.reset(); + m_media_capture_session.reset(); +} + +void camera_settings_dialog::handle_handler_change(int index) +{ + if (index < 0 || !ui->combo_handlers->itemData(index).canConvert()) + { + ui->combo_settings->clear(); + ui->combo_camera->clear(); + enable_combos(); + return; + } + + m_handler = static_cast(ui->combo_handlers->itemData(index).value()); + + ui->combo_settings->blockSignals(true); + ui->combo_camera->blockSignals(true); + + ui->combo_settings->clear(); + ui->combo_camera->clear(); + + switch (m_handler) + { + case camera_handler::qt: + { + for (const QCameraDevice& camera_info : QMediaDevices::videoInputs()) + { + if (camera_info.isNull()) continue; + ui->combo_camera->addItem(camera_info.description(), QVariant::fromValue(camera_info)); + camera_log.notice("Found camera: '%s'", camera_info.description()); + } + break; + } +#ifdef HAVE_SDL3 + case camera_handler::sdl: + { + if (!sdl_instance::get_instance().initialize()) + { + camera_log.error("Could not initialize SDL"); + break; + } + + // Log camera drivers + sdl_camera_handler::get_drivers(); + + // Get cameras + const std::map cameras = sdl_camera_handler::get_cameras(); + + // Add cameras + for (const auto& [camera_id, name] : cameras) + { + ui->combo_camera->addItem(QString::fromStdString(name), QVariant::fromValue(static_cast(camera_id))); + } + break; + } +#endif + default: + fmt::throw_exception("Unexpected camera handler %d", static_cast(m_handler)); + } + + ui->combo_settings->blockSignals(false); + ui->combo_camera->blockSignals(false); + + enable_combos(); } void camera_settings_dialog::handle_camera_change(int index) { - if (index < 0 || !ui->combo_camera->itemData(index).canConvert()) + if (index < 0) { ui->combo_settings->clear(); return; } - const QCameraDevice camera_info = ui->combo_camera->itemData(index).value(); + reset_cameras(); + + switch (m_handler) + { + case camera_handler::qt: + handle_qt_camera_change(ui->combo_camera->itemData(index)); + break; +#ifdef HAVE_SDL3 + case camera_handler::sdl: + handle_sdl_camera_change(ui->combo_camera->itemText(index), ui->combo_camera->itemData(index)); + break; +#endif + default: + fmt::throw_exception("Unexpected camera handler %d", static_cast(m_handler)); + } +} + +void camera_settings_dialog::handle_settings_change(int index) +{ + if (index < 0) + { + return; + } + + if (!gui::utils::check_camera_permission(this, + [this, index](){ handle_settings_change(index); }, + [this](){ QMessageBox::warning(this, tr("Camera permissions denied!"), tr("RPCS3 has no permissions to access cameras on this device.")); })) + { + return; + } + + switch (m_handler) + { + case camera_handler::qt: + handle_qt_settings_change(ui->combo_settings->itemData(index)); + break; +#ifdef HAVE_SDL3 + case camera_handler::sdl: + handle_sdl_settings_change(ui->combo_settings->itemData(index)); + break; +#endif + default: + fmt::throw_exception("Unexpected camera handler %d", static_cast(m_handler)); + } +} + +void camera_settings_dialog::handle_qt_camera_change(const QVariant& item_data) +{ + if (!item_data.canConvert()) + { + ui->combo_settings->clear(); + return; + } + + const QCameraDevice camera_info = item_data.value(); if (camera_info.isNull()) { @@ -119,10 +350,10 @@ void camera_settings_dialog::handle_camera_change(int index) return; } - m_camera.reset(new QCamera(camera_info)); - m_media_capture_session.reset(new QMediaCaptureSession(nullptr)); + m_camera = std::make_unique(camera_info); + m_media_capture_session = std::make_unique(nullptr); m_media_capture_session->setCamera(m_camera.get()); - m_media_capture_session->setVideoSink(ui->videoWidget->videoSink()); + m_media_capture_session->setVideoOutput(ui->videoWidget); if (!m_camera->isAvailable()) { @@ -173,14 +404,14 @@ void camera_settings_dialog::handle_camera_change(int index) int index = 0; bool success = false; const std::string key = camera_info.id().toStdString(); - cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(key, success); + cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(fmt::format("%s", camera_handler::qt), key, success); if (success) { camera_log.notice("Found config entry for camera \"%s\"", key); // Select matching drowdown entry - const double epsilon = 0.001; + constexpr double epsilon = 0.001; for (int i = 0; i < ui->combo_settings->count(); i++) { @@ -202,19 +433,10 @@ void camera_settings_dialog::handle_camera_change(int index) ui->combo_settings->setCurrentIndex(std::max(0, index)); ui->combo_settings->setEnabled(true); - - // Update config to match user interface outcome - const QCameraFormat setting = ui->combo_settings->currentData().value(); - cfg_setting.width = setting.resolution().width(); - cfg_setting.height = setting.resolution().height(); - cfg_setting.min_fps = setting.minFrameRate(); - cfg_setting.max_fps = setting.maxFrameRate(); - cfg_setting.format = static_cast(setting.pixelFormat()); - g_cfg_camera.set_camera_setting(key, cfg_setting); } } -void camera_settings_dialog::handle_settings_change(int index) +void camera_settings_dialog::handle_qt_settings_change(const QVariant& item_data) { if (!m_camera) { @@ -227,16 +449,9 @@ void camera_settings_dialog::handle_settings_change(int index) return; } - if (!gui::utils::check_camera_permission(this, - [this, index](){ handle_settings_change(index); }, - [this](){ QMessageBox::warning(this, tr("Camera permissions denied!"), tr("RPCS3 has no permissions to access cameras on this device.")); })) + if (item_data.canConvert() && ui->combo_camera->currentData().canConvert()) { - return; - } - - if (index >= 0 && ui->combo_settings->itemData(index).canConvert() && ui->combo_camera->currentData().canConvert()) - { - const QCameraFormat setting = ui->combo_settings->itemData(index).value(); + const QCameraFormat setting = item_data.value(); if (!setting.isNull()) { m_camera->setCameraFormat(setting); @@ -248,12 +463,212 @@ void camera_settings_dialog::handle_settings_change(int index) cfg_setting.min_fps = setting.minFrameRate(); cfg_setting.max_fps = setting.maxFrameRate(); cfg_setting.format = static_cast(setting.pixelFormat()); - g_cfg_camera.set_camera_setting(ui->combo_camera->currentData().value().id().toStdString(), cfg_setting); + cfg_setting.colorspace = 0; + g_cfg_camera.set_camera_setting(fmt::format("%s", camera_handler::qt), ui->combo_camera->currentData().value().id().toStdString(), cfg_setting); } m_camera->start(); } +#ifdef HAVE_SDL3 +void camera_settings_dialog::handle_sdl_camera_change(const QString& name, const QVariant& item_data) +{ + if (!item_data.canConvert()) + { + ui->combo_settings->clear(); + return; + } + + const u32 camera_id = item_data.value(); + + if (!camera_id) + { + ui->combo_settings->clear(); + return; + } + + ui->combo_settings->blockSignals(true); + ui->combo_settings->clear(); + + std::vector settings; + + int num_formats = 0; + if (SDL_CameraSpec** specs = SDL_GetCameraSupportedFormats(camera_id, &num_formats)) + { + if (num_formats <= 0) + { + camera_log.error("No SDL camera specs found"); + } + else + { + for (int i = 0; i < num_formats; i++) + { + if (!specs[i]) continue; + settings.push_back(*specs[i]); + } + } + SDL_free(specs); + } + else + { + camera_log.error("No SDL camera specs found. SDL Error: %s", SDL_GetError()); + } + + std::sort(settings.begin(), settings.end(), [](const SDL_CameraSpec& l, const SDL_CameraSpec& r) -> bool + { + const f32 l_fps = l.framerate_numerator / static_cast(l.framerate_denominator); + const f32 r_fps = r.framerate_numerator / static_cast(r.framerate_denominator); + + if (l.width > r.width) return true; + if (l.width < r.width) return false; + if (l.height > r.height) return true; + if (l.height < r.height) return false; + if (l_fps > r_fps) return true; + if (l_fps < r_fps) return false; + if (l.format > r.format) return true; + if (l.format < r.format) return false; + if (l.colorspace > r.colorspace) return true; + if (l.colorspace < r.colorspace) return false; + return false; + }); + + for (const SDL_CameraSpec& setting : settings) + { + const f32 fps = setting.framerate_numerator / static_cast(setting.framerate_denominator); + const QString description = tr("%0x%1, %2 FPS, Format=%3") + .arg(setting.width) + .arg(setting.height) + .arg(fps) + .arg(sdl_pixelformat_to_string(setting.format)); + ui->combo_settings->addItem(description, QVariant::fromValue(setting)); + } + ui->combo_settings->blockSignals(false); + + if (ui->combo_settings->count() == 0) + { + ui->combo_settings->setEnabled(false); + return; + } + + // Load selected settings from config file + int index = 0; + bool success = false; + cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(fmt::format("%s", camera_handler::sdl), name.toStdString(), success); + + if (success) + { + camera_log.notice("Found config entry for camera \"%s\"", name); + + // Select matching drowdown entry + constexpr double epsilon = 0.001; + + for (int i = 0; i < ui->combo_settings->count(); i++) + { + const SDL_CameraSpec tmp = ui->combo_settings->itemData(i).value(); + const f32 fps = tmp.framerate_numerator / static_cast(tmp.framerate_denominator); + + if (tmp.width == cfg_setting.width && + tmp.height == cfg_setting.height && + fps >= (cfg_setting.min_fps - epsilon) && + fps <= (cfg_setting.min_fps + epsilon) && + fps >= (cfg_setting.max_fps - epsilon) && + fps <= (cfg_setting.max_fps + epsilon) && + tmp.format == static_cast(cfg_setting.format) && + tmp.colorspace == static_cast(cfg_setting.colorspace)) + { + index = i; + break; + } + } + } + + m_sdl_camera_id = camera_id; + + ui->combo_settings->setCurrentIndex(std::max(0, index)); + ui->combo_settings->setEnabled(true); +} + +void camera_settings_dialog::handle_sdl_settings_change(const QVariant& item_data) +{ + reset_cameras(); + + if (item_data.canConvert()) + { + // TODO: SDL converts the image for us. We would have to do this manually if we want to use other formats. + const SDL_CameraSpec setting = item_data.value(); + const SDL_CameraSpec used_spec + { + .format = SDL_PixelFormat::SDL_PIXELFORMAT_RGBA32, + .colorspace = SDL_Colorspace::SDL_COLORSPACE_RGB_DEFAULT, + .width = setting.width, + .height = setting.height, + .framerate_numerator = setting.framerate_numerator, + .framerate_denominator = setting.framerate_denominator + }; + + m_sdl_camera = SDL_OpenCamera(m_sdl_camera_id, &used_spec); + + m_video_frame_input = std::make_unique(); + + m_media_capture_session = std::make_unique(nullptr); + m_media_capture_session->setVideoFrameInput(m_video_frame_input.get()); + m_media_capture_session->setVideoOutput(ui->videoWidget); + + connect(this, &camera_settings_dialog::video_frame_ready, m_video_frame_input.get(), &QVideoFrameInput::sendVideoFrame); + + const f32 fps = setting.framerate_numerator / static_cast(setting.framerate_denominator); + + cfg_camera::camera_setting cfg_setting; + cfg_setting.width = setting.width; + cfg_setting.height = setting.height; + cfg_setting.min_fps = fps; + cfg_setting.max_fps = fps; + cfg_setting.format = static_cast(setting.format); + cfg_setting.colorspace = static_cast(setting.colorspace); + g_cfg_camera.set_camera_setting(fmt::format("%s", camera_handler::sdl), ui->combo_camera->currentText().toStdString(), cfg_setting); + } + + if (!m_sdl_camera) + { + camera_log.error("Failed to open SDL camera %d. SDL Error: %s", m_sdl_camera_id, SDL_GetError()); + QMessageBox::warning(this, tr("Camera not available"), tr("The selected camera is not available.\nIt might be blocked by another application.")); + return; + } + + m_sdl_thread = std::make_unique>>("GUI SDL Capture Thread", [this](){ run_sdl(); }); +} + +void camera_settings_dialog::run_sdl() +{ + camera_log.notice("GUI SDL Capture Thread started"); + + while (thread_ctrl::state() != thread_state::aborting) + { + // Copy latest image into out buffer. + u64 timestamp_ns = 0; + SDL_Surface* frame = SDL_AcquireCameraFrame(m_sdl_camera, ×tamp_ns); + if (!frame) + { + // No new frame + thread_ctrl::wait_for(1000); + continue; + } + + const QImage::Format format = SDL_ISPIXELFORMAT_ALPHA(frame->format) ? QImage::Format_RGBA8888 : QImage::Format_RGB888; + const QImage image = QImage(reinterpret_cast(frame->pixels), frame->w, frame->h, format); + const QImage converted = image.convertToFormat(QImage::Format_RGBA8888); + QVideoFrame video_frame(QVideoFrameFormat(converted.size(), QVideoFrameFormat::Format_RGBA8888)); + video_frame.map(QVideoFrame::WriteOnly); + std::memcpy(video_frame.bits(0), converted.constBits(), converted.sizeInBytes()); + video_frame.unmap(); + + Q_EMIT video_frame_ready(video_frame); + + SDL_ReleaseCameraFrame(m_sdl_camera, frame); + } +} +#endif + void camera_settings_dialog::load_config() { if (!g_cfg_camera.load()) diff --git a/rpcs3/rpcs3qt/camera_settings_dialog.h b/rpcs3/rpcs3qt/camera_settings_dialog.h index da18f64cca..aee45d961f 100644 --- a/rpcs3/rpcs3qt/camera_settings_dialog.h +++ b/rpcs3/rpcs3qt/camera_settings_dialog.h @@ -1,8 +1,24 @@ #pragma once +#include "Emu/system_config.h" +#include "Utilities/Thread.h" + #include #include #include +#include +#include + +#ifdef HAVE_SDL3 +#ifndef _MSC_VER +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wold-style-cast" +#endif +#include "SDL3/SDL.h" +#ifndef _MSC_VER +#pragma GCC diagnostic pop +#endif +#endif namespace Ui { @@ -17,15 +33,38 @@ public: camera_settings_dialog(QWidget* parent = nullptr); virtual ~camera_settings_dialog(); +Q_SIGNALS: + void video_frame_ready(const QVideoFrame& frame); + private Q_SLOTS: + void handle_handler_change(int index); void handle_camera_change(int index); void handle_settings_change(int index); private: + void enable_combos(); + void reset_cameras(); + void load_config(); void save_config(); + void handle_qt_camera_change(const QVariant& item_data); + void handle_qt_settings_change(const QVariant& item_data); + +#ifdef HAVE_SDL3 + void handle_sdl_camera_change(const QString& name, const QVariant& item_data); + void handle_sdl_settings_change(const QVariant& item_data); + + void run_sdl(); + + SDL_Camera* m_sdl_camera = nullptr; + SDL_CameraID m_sdl_camera_id = 0; + std::unique_ptr>> m_sdl_thread; + std::unique_ptr m_video_frame_input; +#endif + std::unique_ptr ui; std::unique_ptr m_camera; std::unique_ptr m_media_capture_session; + camera_handler m_handler = camera_handler::qt; }; diff --git a/rpcs3/rpcs3qt/camera_settings_dialog.ui b/rpcs3/rpcs3qt/camera_settings_dialog.ui index 8afe262f22..dfdf6beed2 100644 --- a/rpcs3/rpcs3qt/camera_settings_dialog.ui +++ b/rpcs3/rpcs3qt/camera_settings_dialog.ui @@ -15,7 +15,23 @@ - + + + + + Handler + + + + + + No handlers found + + + + + + @@ -75,10 +91,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Save + QDialogButtonBox::StandardButton::Apply|QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save diff --git a/rpcs3/rpcs3qt/emu_settings.cpp b/rpcs3/rpcs3qt/emu_settings.cpp index 0dc672bc41..82911880fe 100644 --- a/rpcs3/rpcs3qt/emu_settings.cpp +++ b/rpcs3/rpcs3qt/emu_settings.cpp @@ -1129,6 +1129,9 @@ QString emu_settings::GetLocalizedSetting(const QString& original, emu_settings_ case camera_handler::null: return tr("Null", "Camera handler"); case camera_handler::fake: return tr("Fake", "Camera handler"); case camera_handler::qt: return tr("Qt", "Camera handler"); +#ifdef HAVE_SDL3 + case camera_handler::sdl: return tr("SDL", "Camera handler"); +#endif } break; case emu_settings_type::MusicHandler: diff --git a/rpcs3/rpcs3qt/gui_application.cpp b/rpcs3/rpcs3qt/gui_application.cpp index 3024234c17..7b9f1d6689 100644 --- a/rpcs3/rpcs3qt/gui_application.cpp +++ b/rpcs3/rpcs3qt/gui_application.cpp @@ -20,6 +20,10 @@ #include "_discord_utils.h" #endif +#ifdef HAVE_SDL3 +#include "Input/sdl_camera_handler.h" +#endif + #include "Emu/Audio/audio_utils.h" #include "Emu/Io/Null/null_camera_handler.h" #include "Emu/Io/Null/null_music_handler.h" @@ -576,6 +580,12 @@ void gui_application::InitializeCallbacks() { return std::make_shared(); } +#ifdef HAVE_SDL3 + case camera_handler::sdl: + { + return std::make_shared(); + } +#endif } return nullptr; }; diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 9c5562d6a0..d8d03418c9 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -2992,8 +2992,8 @@ void main_window::CreateConnects() connect(ui->confCamerasAct, &QAction::triggered, this, [this]() { - camera_settings_dialog dlg(this); - dlg.exec(); + camera_settings_dialog* dlg = new camera_settings_dialog(this); + dlg->open(); }); connect(ui->confRPCNAct, &QAction::triggered, this, [this]() diff --git a/rpcs3/rpcs3qt/movie_item_base.cpp b/rpcs3/rpcs3qt/movie_item_base.cpp index d413f4d0b3..a2e0fe1dc8 100644 --- a/rpcs3/rpcs3qt/movie_item_base.cpp +++ b/rpcs3/rpcs3qt/movie_item_base.cpp @@ -26,8 +26,8 @@ movie_item_base::~movie_item_base() void movie_item_base::init_pointers() { - m_icon_loading_aborted.reset(new atomic_t(false)); - m_size_on_disk_loading_aborted.reset(new atomic_t(false)); + m_icon_loading_aborted = std::make_shared>(false); + m_size_on_disk_loading_aborted = std::make_shared>(false); } void movie_item_base::set_active(bool active) @@ -67,7 +67,7 @@ void movie_item_base::init_movie() if (lower.endsWith(".gif")) { - m_movie.reset(new QMovie(m_movie_path)); + m_movie = std::make_shared(m_movie_path); m_movie_path.clear(); if (!m_movie->isValid()) @@ -99,17 +99,17 @@ void movie_item_base::init_movie() return; } - m_movie_buffer.reset(new QBuffer(&m_movie_data)); + m_movie_buffer = std::make_unique(&m_movie_data); m_movie_buffer->open(QIODevice::ReadOnly); } - m_video_sink.reset(new QVideoSink()); + m_video_sink = std::make_shared(); QObject::connect(m_video_sink.get(), &QVideoSink::videoFrameChanged, m_video_sink.get(), [this](const QVideoFrame& frame) { m_icon_callback(frame); }); - m_media_player.reset(new QMediaPlayer()); + m_media_player = std::make_unique(); m_media_player->setVideoSink(m_video_sink.get()); m_media_player->setLoops(QMediaPlayer::Infinite); diff --git a/rpcs3/rpcs3qt/qt_camera_handler.cpp b/rpcs3/rpcs3qt/qt_camera_handler.cpp index 5b0caeb642..91f0f1b05c 100644 --- a/rpcs3/rpcs3qt/qt_camera_handler.cpp +++ b/rpcs3/rpcs3qt/qt_camera_handler.cpp @@ -341,14 +341,14 @@ void qt_camera_handler::update_camera_settings() // Load selected settings from config file bool success = false; - cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(camera_id, success); + cfg_camera::camera_setting cfg_setting = g_cfg_camera.get_camera_setting(fmt::format("%s", camera_handler::qt), camera_id, success); if (success) { camera_log.notice("Found config entry for camera \"%s\" (m_camera_id='%s')", camera_id, m_camera_id); // List all available settings and choose the proper value if possible. - const double epsilon = 0.001; + constexpr double epsilon = 0.001; success = false; for (const QCameraFormat& supported_setting : m_camera->cameraDevice().videoFormats()) { diff --git a/rpcs3/rpcs3qt/qt_camera_video_sink.cpp b/rpcs3/rpcs3qt/qt_camera_video_sink.cpp index bcfd573afe..808e27aa59 100644 --- a/rpcs3/rpcs3qt/qt_camera_video_sink.cpp +++ b/rpcs3/rpcs3qt/qt_camera_video_sink.cpp @@ -1,7 +1,6 @@ #include "stdafx.h" #include "qt_camera_video_sink.h" -#include "Emu/Cell/Modules/cellCamera.h" #include "Emu/system_config.h" #include @@ -9,7 +8,7 @@ LOG_CHANNEL(camera_log, "Camera"); qt_camera_video_sink::qt_camera_video_sink(bool front_facing, QObject *parent) - : QVideoSink(parent), m_front_facing(front_facing) + : camera_video_sink(front_facing), QVideoSink(parent) { connect(this, &QVideoSink::videoFrameChanged, this, &qt_camera_video_sink::present); } @@ -36,6 +35,8 @@ bool qt_camera_video_sink::present(const QVideoFrame& frame) // Get image. This usually also converts the image to ARGB32. QImage image = frame.toImage(); + const u32 width = image.isNull() ? 0 : static_cast(image.width()); + const u32 height = image.isNull() ? 0 : static_cast(image.height()); if (image.isNull()) { @@ -44,7 +45,7 @@ bool qt_camera_video_sink::present(const QVideoFrame& frame) else { // Scale image if necessary - if (m_width > 0 && m_height > 0 && m_width != static_cast(image.width()) && m_height != static_cast(image.height())) + if (m_width > 0 && m_height > 0 && m_width != width && m_height != height) { image = image.scaled(m_width, m_height, Qt::AspectRatioMode::IgnoreAspectRatio, Qt::SmoothTransformation); } @@ -80,239 +81,10 @@ bool qt_camera_video_sink::present(const QVideoFrame& frame) } } - const u64 new_size = m_bytesize; - image_buffer& image_buffer = m_image_buffer[m_write_index]; - - // Reset buffer if necessary - if (image_buffer.data.size() != new_size) - { - image_buffer.data.clear(); - } - - // Create buffer if necessary - if (image_buffer.data.empty() && new_size > 0) - { - image_buffer.data.resize(new_size); - image_buffer.width = m_width; - image_buffer.height = m_height; - } - - if (!image_buffer.data.empty() && !image.isNull()) - { - // Convert image to proper layout - // TODO: check if pixel format and bytes per pixel match and convert if necessary - // TODO: implement or improve more conversions - - const u32 width = std::min(image_buffer.width, image.width()); - const u32 height = std::min(image_buffer.height, image.height()); - - switch (m_format) - { - case CELL_CAMERA_RAW8: // The game seems to expect BGGR - { - // Let's use a very simple algorithm to convert the image to raw BGGR - const auto convert_to_bggr = [&image_buffer, &image, width, height](u32 y_begin, u32 y_end) - { - u8* dst = &image_buffer.data[image_buffer.width * y_begin]; - - for (u32 y = y_begin; y < height && y < y_end; y++) - { - const u8* src = image.constScanLine(y); - const bool is_top_pixel = (y % 2) == 0; - - // Split loops (roughly twice the performance by removing one condition) - if (is_top_pixel) - { - for (u32 x = 0; x < width; x++, dst++, src += 4) - { - const bool is_left_pixel = (x % 2) == 0; - - if (is_left_pixel) - { - *dst = src[2]; // Blue - } - else - { - *dst = src[1]; // Green - } - } - } - else - { - for (u32 x = 0; x < width; x++, dst++, src += 4) - { - const bool is_left_pixel = (x % 2) == 0; - - if (is_left_pixel) - { - *dst = src[1]; // Green - } - else - { - *dst = src[0]; // Red - } - } - } - } - }; - - // Use a multithreaded workload. The faster we get this done, the better. - constexpr u32 thread_count = 4; - const u32 lines_per_thread = std::ceil(image_buffer.height / static_cast(thread_count)); - u32 y_begin = 0; - u32 y_end = lines_per_thread; - - QFutureSynchronizer synchronizer; - for (u32 i = 0; i < thread_count; i++) - { - synchronizer.addFuture(QtConcurrent::run(convert_to_bggr, y_begin, y_end)); - y_begin = y_end; - y_end += lines_per_thread; - } - synchronizer.waitForFinished(); - break; - } - //case CELL_CAMERA_YUV422: - case CELL_CAMERA_Y0_U_Y1_V: - case CELL_CAMERA_V_Y1_U_Y0: - { - // Simple RGB to Y0_U_Y1_V conversion from stackoverflow. - const auto convert_to_yuv422 = [&image_buffer, &image, width, height, format = m_format](u32 y_begin, u32 y_end) - { - constexpr int yuv_bytes_per_pixel = 2; - const int yuv_pitch = image_buffer.width * yuv_bytes_per_pixel; - - const int y0_offset = (format == CELL_CAMERA_Y0_U_Y1_V) ? 0 : 3; - const int u_offset = (format == CELL_CAMERA_Y0_U_Y1_V) ? 1 : 2; - const int y1_offset = (format == CELL_CAMERA_Y0_U_Y1_V) ? 2 : 1; - const int v_offset = (format == CELL_CAMERA_Y0_U_Y1_V) ? 3 : 0; - - for (u32 y = y_begin; y < height && y < y_end; y++) - { - const u8* src = image.constScanLine(y); - u8* yuv_row_ptr = &image_buffer.data[y * yuv_pitch]; - - for (u32 x = 0; x < width - 1; x += 2, src += 8) - { - const float r1 = src[0]; - const float g1 = src[1]; - const float b1 = src[2]; - const float r2 = src[4]; - const float g2 = src[5]; - const float b2 = src[6]; - - const int y0 = (0.257f * r1) + (0.504f * g1) + (0.098f * b1) + 16.0f; - const int u = -(0.148f * r1) - (0.291f * g1) + (0.439f * b1) + 128.0f; - const int v = (0.439f * r1) - (0.368f * g1) - (0.071f * b1) + 128.0f; - const int y1 = (0.257f * r2) + (0.504f * g2) + (0.098f * b2) + 16.0f; - - const int yuv_index = x * yuv_bytes_per_pixel; - yuv_row_ptr[yuv_index + y0_offset] = static_cast(std::clamp(y0, 0, 255)); - yuv_row_ptr[yuv_index + u_offset] = static_cast(std::clamp( u, 0, 255)); - yuv_row_ptr[yuv_index + y1_offset] = static_cast(std::clamp(y1, 0, 255)); - yuv_row_ptr[yuv_index + v_offset] = static_cast(std::clamp( v, 0, 255)); - } - } - }; - - // Use a multithreaded workload. The faster we get this done, the better. - constexpr u32 thread_count = 4; - const u32 lines_per_thread = std::ceil(image_buffer.height / static_cast(thread_count)); - u32 y_begin = 0; - u32 y_end = lines_per_thread; - - QFutureSynchronizer synchronizer; - for (u32 i = 0; i < thread_count; i++) - { - synchronizer.addFuture(QtConcurrent::run(convert_to_yuv422, y_begin, y_end)); - y_begin = y_end; - y_end += lines_per_thread; - } - synchronizer.waitForFinished(); - break; - } - case CELL_CAMERA_JPG: - case CELL_CAMERA_RGBA: - case CELL_CAMERA_RAW10: - case CELL_CAMERA_YUV420: - case CELL_CAMERA_FORMAT_UNKNOWN: - default: - std::memcpy(image_buffer.data.data(), image.constBits(), std::min(image_buffer.data.size(), image.height() * image.bytesPerLine())); - break; - } - } + camera_video_sink::present(width, height, image.bytesPerLine(), 4, [&image](u32 y){ return image.constScanLine(y); }); // Unmap frame memory tmp.unmap(); - camera_log.trace("Wrote image to video surface. index=%d, m_frame_number=%d, width=%d, height=%d, bytesize=%d", - m_write_index, m_frame_number.load(), m_width, m_height, m_bytesize); - - // Toggle write/read index - std::lock_guard lock(m_mutex); - image_buffer.frame_number = m_frame_number++; - m_write_index = read_index(); - return true; } - -void qt_camera_video_sink::set_format(s32 format, u32 bytesize) -{ - camera_log.notice("Setting format: format=%d, bytesize=%d", format, bytesize); - - m_format = format; - m_bytesize = bytesize; -} - -void qt_camera_video_sink::set_resolution(u32 width, u32 height) -{ - camera_log.notice("Setting resolution: width=%d, height=%d", width, height); - - m_width = width; - m_height = height; -} - -void qt_camera_video_sink::set_mirrored(bool mirrored) -{ - camera_log.notice("Setting mirrored: mirrored=%d", mirrored); - - m_mirrored = mirrored; -} - -u64 qt_camera_video_sink::frame_number() const -{ - return m_frame_number.load(); -} - -void qt_camera_video_sink::get_image(u8* buf, u64 size, u32& width, u32& height, u64& frame_number, u64& bytes_read) -{ - // Lock read buffer - std::lock_guard lock(m_mutex); - const image_buffer& image_buffer = m_image_buffer[read_index()]; - - width = image_buffer.width; - height = image_buffer.height; - frame_number = image_buffer.frame_number; - - // Copy to out buffer - if (buf && !image_buffer.data.empty()) - { - bytes_read = std::min(image_buffer.data.size(), size); - std::memcpy(buf, image_buffer.data.data(), bytes_read); - - if (image_buffer.data.size() != size) - { - camera_log.error("Buffer size mismatch: in=%d, out=%d. Cropping to incoming size. Please contact a developer.", size, image_buffer.data.size()); - } - } - else - { - bytes_read = 0; - } -} - -u32 qt_camera_video_sink::read_index() const -{ - // The read buffer index cannot be the same as the write index - return (m_write_index + 1u) % ::narrow(m_image_buffer.size()); -} diff --git a/rpcs3/rpcs3qt/qt_camera_video_sink.h b/rpcs3/rpcs3qt/qt_camera_video_sink.h index e3f405b55c..8f228bb94f 100644 --- a/rpcs3/rpcs3qt/qt_camera_video_sink.h +++ b/rpcs3/rpcs3qt/qt_camera_video_sink.h @@ -1,50 +1,15 @@ #pragma once -#include "util/atomic.hpp" -#include "util/types.hpp" +#include "Input/camera_video_sink.h" + #include #include -#include -#include -#include - -class qt_camera_video_sink final : public QVideoSink +class qt_camera_video_sink final : public camera_video_sink, public QVideoSink { public: qt_camera_video_sink(bool front_facing, QObject *parent = nullptr); virtual ~qt_camera_video_sink(); bool present(const QVideoFrame& frame); - - void set_format(s32 format, u32 bytesize); - void set_resolution(u32 width, u32 height); - void set_mirrored(bool mirrored); - - u64 frame_number() const; - - void get_image(u8* buf, u64 size, u32& width, u32& height, u64& frame_number, u64& bytes_read); - -private: - u32 read_index() const; - - bool m_front_facing = false; - bool m_mirrored = false; // Set by cellCamera - s32 m_format = 2; // CELL_CAMERA_RAW8, set by cellCamera - u32 m_bytesize = 0; - u32 m_width = 640; - u32 m_height = 480; - - std::mutex m_mutex; - atomic_t m_frame_number{0}; - u32 m_write_index{0}; - - struct image_buffer - { - u64 frame_number = 0; - u32 width = 0; - u32 height = 0; - std::vector data; - }; - std::array m_image_buffer; };