Add SDL camera handler
Some checks are pending
Generate Translation Template / Generate Translation Template (push) Waiting to run
Build RPCS3 / RPCS3 Linux ${{ matrix.os }} ${{ matrix.compiler }} (/rpcs3/.ci/build-linux-aarch64.sh, gcc, rpcs3/rpcs3-ci-jammy-aarch64:1.7, ubuntu-24.04-arm) (push) Waiting to run
Build RPCS3 / RPCS3 Linux ${{ matrix.os }} ${{ matrix.compiler }} (/rpcs3/.ci/build-linux.sh, gcc, rpcs3/rpcs3-ci-jammy:1.7, ubuntu-24.04) (push) Waiting to run
Build RPCS3 / RPCS3 Linux ${{ matrix.os }} ${{ matrix.compiler }} (a1d35836e8d45bfc6f63c26f0a3e5d46ef622fe1, rpcs3/rpcs3-binaries-linux-arm64, /rpcs3/.ci/build-linux-aarch64.sh, clang, rpcs3/rpcs3-ci-jammy-aarch64:1.7, ubuntu-24.04-arm) (push) Waiting to run
Build RPCS3 / RPCS3 Linux ${{ matrix.os }} ${{ matrix.compiler }} (d812f1254a1157c80fd402f94446310560f54e5f, rpcs3/rpcs3-binaries-linux, /rpcs3/.ci/build-linux.sh, clang, rpcs3/rpcs3-ci-jammy:1.7, ubuntu-24.04) (push) Waiting to run
Build RPCS3 / RPCS3 Mac ${{ matrix.name }} (51ae32f468089a8169aaf1567de355ff4a3e0842, rpcs3/rpcs3-binaries-mac, .ci/build-mac.sh, Intel) (push) Waiting to run
Build RPCS3 / RPCS3 Mac ${{ matrix.name }} (8e21bdbc40711a3fccd18fbf17b742348b0f4281, rpcs3/rpcs3-binaries-mac-arm64, .ci/build-mac-arm64.sh, Apple Silicon) (push) Waiting to run
Build RPCS3 / RPCS3 Windows (push) Waiting to run
Build RPCS3 / RPCS3 Windows Clang (win64, clang, clang64) (push) Waiting to run
Build RPCS3 / RPCS3 FreeBSD (push) Waiting to run

This commit is contained in:
Megamouse 2025-01-27 20:32:29 +01:00
parent ea6bb77d57
commit 643d1102cc
18 changed files with 1192 additions and 5 deletions

View file

@ -1501,8 +1501,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;

View file

@ -80,9 +80,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);

View file

@ -267,6 +267,7 @@ struct cfg_root : cfg::node
cfg::_enum<fake_camera_type> camera_type{ this, "Camera type", fake_camera_type::unknown };
cfg::_enum<camera_flip> 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_handler> move{ this, "Move", move_handler::null, true };
cfg::_enum<buzz_handler> buzz{ this, "Buzz emulated controller", buzz_handler::null };
cfg::_enum<turntable_handler> turntable{this, "Turntable emulated controller", turntable_handler::null};

View file

@ -358,6 +358,9 @@ void fmt_class_string<camera_handler>::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;

View file

@ -116,7 +116,10 @@ enum class camera_handler
{
null,
fake,
qt
qt,
#ifdef HAVE_SDL3
sdl,
#endif
};
enum class camera_flip

View file

@ -0,0 +1,490 @@
#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");
#if !(SDL_VERSION_ATLEAST(3, 4, 0))
namespace SDL_CameraPermissionState
{
constexpr int SDL_CAMERA_PERMISSION_STATE_DENIED = -1;
constexpr int SDL_CAMERA_PERMISSION_STATE_PENDING = 0;
constexpr int SDL_CAMERA_PERMISSION_STATE_APPROVED = 1;
}
#endif
template <>
void fmt_class_string<SDL_CameraSpec>::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<u32>(spec.format), static_cast<u32>(spec.colorspace), spec.width, spec.height,
spec.framerate_numerator, spec.framerate_denominator, spec.framerate_numerator / static_cast<f32>(spec.framerate_denominator));
}
std::vector<std::string> sdl_camera_handler::get_drivers()
{
std::vector<std::string> 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_CameraID, std::string> sdl_camera_handler::get_cameras()
{
int camera_count = 0;
if (SDL_CameraID* cameras = SDL_GetCameras(&camera_count))
{
std::map<SDL_CameraID, std::string> 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);
if (camera_map.empty())
{
camera_log.notice("No SDL cameras found");
}
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<int>(m_width),
.height = static_cast<int>(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<f64>(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<SDL_PixelFormat>(cfg_setting.format) &&
spec.colorspace == static_cast<SDL_Colorspace>(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;
if (const SDL_PropertiesID property_id = SDL_GetCameraProperties(m_camera); property_id != 0)
{
if (!SDL_EnumerateProperties(property_id, [](void* /*userdata*/, SDL_PropertiesID /*props*/, const char* name)
{
if (name) camera_log.notice("SDL camera property available: %s", name);
}, nullptr))
{
camera_log.warning("SDL_EnumerateProperties failed. SDL Error: %s", SDL_GetError());
}
}
else
{
camera_log.warning("SDL_GetCameraProperties failed. SDL Error: %s", SDL_GetError());
}
m_video_sink = std::make_unique<sdl_camera_video_sink>(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 auto camera_permission = SDL_GetCameraPermissionState(m_camera);
switch (camera_permission)
{
case SDL_CameraPermissionState::SDL_CAMERA_PERMISSION_STATE_DENIED:
camera_log.error("Camera permission denied");
set_state(camera_handler_state::closed);
reset();
return;
case SDL_CameraPermissionState::SDL_CAMERA_PERMISSION_STATE_PENDING:
// TODO: try to get permission
break;
case SDL_CameraPermissionState::SDL_CAMERA_PERMISSION_STATE_APPROVED:
break;
default:
fmt::throw_exception("Unknown SDL_CameraPermissionState %d", static_cast<s32>(camera_permission));
}
// 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<int>(current_state));
}
return current_state;
}
#endif

View file

@ -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 <map>
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<std::string> get_drivers();
static std::map<SDL_CameraID, std::string> 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<sdl_camera_video_sink> m_video_sink;
};
#endif

View file

@ -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<std::thread>(&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<u32>(0, m_width * bytes_per_pixel);
const u8* pixels = reinterpret_cast<const u8*>(frame->pixels);
bool use_buffer = false;
// Scale image if necessary
const bool scale_image = m_width > 0 && m_height > 0 && m_width != static_cast<u32>(frame->w) && m_height != static_cast<u32>(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<f32>(m_width);
const f32 scale_y = frame->h / static_cast<f32>(m_height);
if (flip_horizontally && flip_vertically)
{
for (u32 y = 0; y < m_height; y++)
{
const u32 src_y = frame->h - static_cast<u32>(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<u32>(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<u32>(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<u32>(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<u32>(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<u32>(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<u32>(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<u32>(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, &timestamp_ns);
if (!frame)
{
// No new frame
std::this_thread::sleep_for(100us);
continue;
}
present(frame);
SDL_ReleaseCameraFrame(m_camera, frame);
}
}
#endif

View file

@ -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 <thread>
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<u8> m_buffer;
atomic_t<bool> m_terminate = false;
SDL_Camera* m_camera = nullptr;
std::unique_ptr<std::thread> m_thread;
};
#endif

View file

@ -102,7 +102,7 @@ bool sdl_instance::initialize_impl()
set_hint(SDL_HINT_JOYSTICK_HIDAPI_PS3, "1");
#endif
if (!SDL_Init(SDL_INIT_GAMEPAD | SDL_INIT_HAPTIC))
if (!SDL_Init(SDL_INIT_GAMEPAD | SDL_INIT_HAPTIC | SDL_INIT_CAMERA))
{
sdl_log.error("Could not initialize! SDL Error: %s", SDL_GetError());
return false;

View file

@ -102,6 +102,9 @@ void headless_application::InitializeCallbacks()
return std::make_shared<null_camera_handler>();
}
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());
}

View file

@ -201,6 +201,8 @@
<ClCompile Include="Input\raw_mouse_handler.cpp" />
<ClCompile Include="Input\ps_move_handler.cpp" />
<ClCompile Include="Input\sdl_pad_handler.cpp" />
<ClCompile Include="Input\sdl_camera_handler.cpp" />
<ClCompile Include="Input\sdl_camera_video_sink.cpp" />
<ClCompile Include="Input\sdl_instance.cpp" />
<ClCompile Include="Input\skateboard_pad_handler.cpp" />
<ClCompile Include="main.cpp" />
@ -1070,6 +1072,8 @@
<ClInclude Include="Input\raw_mouse_handler.h" />
<ClInclude Include="Input\ps_move_handler.h" />
<ClInclude Include="Input\sdl_pad_handler.h" />
<ClInclude Include="Input\sdl_camera_handler.h" />
<ClInclude Include="Input\sdl_camera_video_sink.h" />
<ClInclude Include="Input\sdl_instance.h" />
<ClInclude Include="Input\skateboard_pad_handler.h" />
<ClInclude Include="main_application.h" />

View file

@ -1248,6 +1248,12 @@
<ClCompile Include="Input\camera_video_sink.cpp">
<Filter>Io\camera</Filter>
</ClCompile>
<ClCompile Include="Input\sdl_camera_handler.cpp">
<Filter>Io\camera</Filter>
</ClCompile>
<ClCompile Include="Input\sdl_camera_video_sink.cpp">
<Filter>Io\camera</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Input\ds4_pad_handler.h">
@ -1481,6 +1487,12 @@
<ClInclude Include="Input\camera_video_sink.h">
<Filter>Io\camera</Filter>
</ClInclude>
<ClInclude Include="Input\sdl_camera_handler.h">
<Filter>Io\camera</Filter>
</ClInclude>
<ClInclude Include="Input\sdl_camera_video_sink.h">
<Filter>Io\camera</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClInclude Include="resource.h">

View file

@ -163,8 +163,10 @@ add_library(rpcs3_ui STATIC
../Input/ps_move_tracker.cpp
../Input/raw_mouse_config.cpp
../Input/raw_mouse_handler.cpp
../Input/sdl_pad_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

View file

@ -12,6 +12,11 @@
#include <QPushButton>
#include <QVideoSink>
#ifdef HAVE_SDL3
#include "Input/sdl_instance.h"
#include "Input/sdl_camera_handler.h"
#endif
LOG_CHANNEL(camera_log, "Camera");
template <>
@ -56,6 +61,81 @@ void fmt_class_string<QVideoFrameFormat::PixelFormat>::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";
case SDL_PixelFormat::SDL_PIXELFORMAT_MJPG: return "MJPG";
default: return QObject::tr("Unknown: %0").arg(static_cast<int>(format));
}
}
#endif
Q_DECLARE_METATYPE(QCameraDevice);
camera_settings_dialog::camera_settings_dialog(QWidget* parent)
@ -69,6 +149,9 @@ camera_settings_dialog::camera_settings_dialog(QWidget* parent)
load_config();
ui->combo_handlers->addItem("Qt", QVariant::fromValue(static_cast<int>(camera_handler::qt)));
#ifdef HAVE_SDL3
ui->combo_handlers->addItem("SDL", QVariant::fromValue(static_cast<int>(camera_handler::sdl)));
#endif
connect(ui->combo_handlers, &QComboBox::currentIndexChanged, this, &camera_settings_dialog::handle_handler_change);
connect(ui->combo_camera, &QComboBox::currentIndexChanged, this, &camera_settings_dialog::handle_camera_change);
@ -115,6 +198,24 @@ void camera_settings_dialog::reset_cameras()
{
m_media_capture_session.reset();
m_camera.reset();
#ifdef HAVE_SDL3
m_video_frame_input.reset();
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;
}
#endif
}
void camera_settings_dialog::handle_handler_change(int index)
@ -149,6 +250,29 @@ void camera_settings_dialog::handle_handler_change(int index)
}
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<SDL_CameraID, std::string> 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<u32>(camera_id)));
}
break;
}
#endif
default:
fmt::throw_exception("Unexpected camera handler %d", static_cast<int>(m_handler));
}
@ -174,6 +298,11 @@ void camera_settings_dialog::handle_camera_change(int index)
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<int>(m_handler));
}
@ -198,6 +327,11 @@ void camera_settings_dialog::handle_settings_change(int index)
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<int>(m_handler));
}
@ -339,6 +473,230 @@ void camera_settings_dialog::handle_qt_settings_change(const QVariant& item_data
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<u32>())
{
ui->combo_settings->clear();
return;
}
const u32 camera_id = item_data.value<u32>();
if (!camera_id)
{
ui->combo_settings->clear();
return;
}
ui->combo_settings->blockSignals(true);
ui->combo_settings->clear();
std::vector<SDL_CameraSpec> 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<f32>(l.framerate_denominator);
const f32 r_fps = r.framerate_numerator / static_cast<f32>(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<f32>(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 dropdown entry
constexpr double epsilon = 0.001;
for (int i = 0; i < ui->combo_settings->count(); i++)
{
const QVariant var = ui->combo_settings->itemData(i);
if (!var.canConvert<SDL_CameraSpec>())
{
camera_log.error("Failed to convert itemData to SDL_CameraSpec");
continue;
}
const SDL_CameraSpec tmp = var.value<SDL_CameraSpec>();
const f32 fps = tmp.framerate_numerator / static_cast<f32>(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<SDL_PixelFormat>(cfg_setting.format) &&
tmp.colorspace == static_cast<SDL_Colorspace>(cfg_setting.colorspace))
{
index = i;
break;
}
}
}
m_sdl_camera_id = camera_id;
ui->combo_settings->setCurrentIndex(std::max<int>(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<SDL_CameraSpec>())
{
// 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<SDL_CameraSpec>();
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<QVideoFrameInput>();
m_media_capture_session = std::make_unique<QMediaCaptureSession>(nullptr);
m_media_capture_session->setVideoFrameInput(m_video_frame_input.get());
m_media_capture_session->setVideoOutput(ui->videoWidget);
connect(this, &camera_settings_dialog::sdl_frame_ready, m_video_frame_input.get(), [this]()
{
// It was observed that connecting sendVideoFrame directly can soft-lock the software.
// So let's just create the video frame here and call it manually.
std::unique_lock lock(m_sdl_image_mutex, std::defer_lock);
if (lock.try_lock() && m_video_frame_input && !m_sdl_image.isNull())
{
const QVideoFrame video_frame(m_sdl_image);
if (video_frame.isValid())
{
m_video_frame_input->sendVideoFrame(video_frame);
}
}
});
const f32 fps = setting.framerate_numerator / static_cast<f32>(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<int>(setting.format);
cfg_setting.colorspace = static_cast<int>(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<named_thread<std::function<void()>>>("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, &timestamp_ns);
if (!frame)
{
// No new frame
thread_ctrl::wait_for(1000);
continue;
}
{
// Map image
const QImage::Format format = SDL_ISPIXELFORMAT_ALPHA(frame->format) ? QImage::Format_RGBA8888 : QImage::Format_RGB888;
const QImage image = QImage(reinterpret_cast<const u8*>(frame->pixels), frame->w, frame->h, format);
// Copy image to prevent memory access violations
{
std::lock_guard lock(m_sdl_image_mutex);
m_sdl_image = image.copy();
}
// Notify UI
Q_EMIT sdl_frame_ready();
}
SDL_ReleaseCameraFrame(m_sdl_camera, frame);
}
}
#endif
void camera_settings_dialog::load_config()
{
if (!g_cfg_camera.load())

View file

@ -1,10 +1,25 @@
#pragma once
#include "Emu/system_config_types.h"
#include "Utilities/Thread.h"
#include <QCamera>
#include <QDialog>
#include <QMediaCaptureSession>
#include <QVideoFrameInput>
#include <mutex>
#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
{
@ -19,6 +34,9 @@ public:
camera_settings_dialog(QWidget* parent = nullptr);
virtual ~camera_settings_dialog();
Q_SIGNALS:
void sdl_frame_ready();
private Q_SLOTS:
void handle_handler_change(int index);
void handle_camera_change(int index);
@ -34,6 +52,20 @@ private:
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;
QImage m_sdl_image;
std::mutex m_sdl_image_mutex;
std::unique_ptr<named_thread<std::function<void()>>> m_sdl_thread;
std::unique_ptr<QVideoFrameInput> m_video_frame_input;
#endif
std::unique_ptr<Ui::camera_settings_dialog> ui;
std::unique_ptr<QCamera> m_camera;
std::unique_ptr<QMediaCaptureSession> m_media_capture_session;

View file

@ -1119,6 +1119,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:

View file

@ -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/Cell/Modules/cellSysutil.h"
#include "Emu/Io/Null/null_camera_handler.h"
@ -696,6 +700,12 @@ void gui_application::InitializeCallbacks()
{
return std::make_shared<qt_camera_handler>();
}
#ifdef HAVE_SDL3
case camera_handler::sdl:
{
return std::make_shared<sdl_camera_handler>();
}
#endif
}
return nullptr;
};