mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-03-27 13:50:06 -06:00
Initial camera (Part 3 of 0.15.1 branch) (#4156)
* using new emulator_settings * the default user is now just player one * transfer install, addon dirs * fix load custom config issue * initial openal backend * linux fix? * camera module updated --------- Co-authored-by: kalaposfos13 <153381648+kalaposfos13@users.noreply.github.com>
This commit is contained in:
parent
060703f627
commit
880445c2ce
@ -3,17 +3,27 @@
|
||||
|
||||
#include "common/elf_info.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/emulator_settings.h"
|
||||
#include "core/libraries/camera/camera.h"
|
||||
#include "core/libraries/camera/camera_error.h"
|
||||
#include "core/libraries/error_codes.h"
|
||||
#include "core/libraries/kernel/process.h"
|
||||
#include "core/libraries/libs.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <thread>
|
||||
#include "SDL3/SDL_camera.h"
|
||||
|
||||
namespace Libraries::Camera {
|
||||
|
||||
static bool g_library_opened = false;
|
||||
static s32 g_firmware_version = 0;
|
||||
static s32 g_handles = 0;
|
||||
static constexpr s32 c_width = 1280, c_height = 800;
|
||||
|
||||
SDL_Camera* sdl_camera = nullptr;
|
||||
OrbisCameraConfigExtention output_config0, output_config1;
|
||||
|
||||
s32 PS4_SYSV_ABI sceCameraAccGetData() {
|
||||
LOG_ERROR(Lib_Camera, "(STUBBED) called");
|
||||
@ -325,16 +335,126 @@ s32 PS4_SYSV_ABI sceCameraGetExposureGain(s32 handle, OrbisCameraChannel channel
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
static std::vector<u16> raw16_buffer1, raw16_buffer2;
|
||||
static std::vector<u8> raw8_buffer1, raw8_buffer2;
|
||||
|
||||
static void ConvertRGBA8888ToRAW16(const u8* src, u16* dst, int width, int height) {
|
||||
for (int y = 0; y < height; ++y) {
|
||||
const u8* row = src + y * width * 4;
|
||||
u16* outRow = dst + y * width;
|
||||
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const u8* px = row + x * 4;
|
||||
|
||||
u16 b = u16(px[1]) << 4;
|
||||
u16 g = u16(px[2]) << 4;
|
||||
u16 r = u16(px[3]) << 4;
|
||||
|
||||
// BGGR Bayer layout
|
||||
// B G
|
||||
// G R
|
||||
bool evenRow = (y & 1) == 0;
|
||||
bool evenCol = (x & 1) == 0;
|
||||
|
||||
if (evenRow && evenCol) {
|
||||
outRow[x] = b;
|
||||
} else if (evenRow && !evenCol) {
|
||||
outRow[x] = g;
|
||||
} else if (!evenRow && evenCol) {
|
||||
outRow[x] = g;
|
||||
} else {
|
||||
outRow[x] = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void ConvertRGBA8888ToRAW8(const u8* src, u8* dst, int width, int height) {
|
||||
for (int y = 0; y < height; ++y) {
|
||||
const u8* row = src + y * width * 4;
|
||||
u8* outRow = dst + y * width;
|
||||
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const u8* px = row + x * 4;
|
||||
|
||||
u8 b = px[1];
|
||||
u8 g = px[2];
|
||||
u8 r = px[3];
|
||||
|
||||
// BGGR Bayer layout
|
||||
// B G
|
||||
// G R
|
||||
bool evenRow = (y & 1) == 0;
|
||||
bool evenCol = (x & 1) == 0;
|
||||
|
||||
if (evenRow && evenCol) {
|
||||
outRow[x] = b;
|
||||
} else if (evenRow && !evenCol) {
|
||||
outRow[x] = g;
|
||||
} else if (!evenRow && evenCol) {
|
||||
outRow[x] = g;
|
||||
} else {
|
||||
outRow[x] = r;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceCameraGetFrameData(s32 handle, OrbisCameraFrameData* frame_data) {
|
||||
LOG_DEBUG(Lib_Camera, "called");
|
||||
if (handle < 1 || frame_data == nullptr || frame_data->sizeThis > 584) {
|
||||
return ORBIS_CAMERA_ERROR_PARAM;
|
||||
}
|
||||
if (!g_library_opened) {
|
||||
if (!g_library_opened || !sdl_camera) {
|
||||
return ORBIS_CAMERA_ERROR_NOT_OPEN;
|
||||
}
|
||||
if (EmulatorSettings.GetCameraId() == -1) {
|
||||
return ORBIS_CAMERA_ERROR_NOT_CONNECTED;
|
||||
}
|
||||
Uint64 timestampNS = 0;
|
||||
static SDL_Surface* frame = nullptr;
|
||||
if (frame) { // release previous frame, if it exists
|
||||
SDL_ReleaseCameraFrame(sdl_camera, frame);
|
||||
}
|
||||
frame = SDL_AcquireCameraFrame(sdl_camera, ×tampNS);
|
||||
|
||||
return ORBIS_CAMERA_ERROR_NOT_CONNECTED;
|
||||
if (!frame) {
|
||||
return ORBIS_CAMERA_ERROR_BUSY;
|
||||
}
|
||||
|
||||
switch (output_config0.format.formatLevel0) {
|
||||
case ORBIS_CAMERA_FORMAT_YUV422:
|
||||
frame_data->pFramePointerList[0][0] = frame->pixels;
|
||||
break;
|
||||
case ORBIS_CAMERA_FORMAT_RAW16:
|
||||
ConvertRGBA8888ToRAW16((u8*)frame->pixels, raw16_buffer1.data(), c_width, c_height);
|
||||
frame_data->pFramePointerList[0][0] = raw16_buffer1.data();
|
||||
break;
|
||||
case ORBIS_CAMERA_FORMAT_RAW8:
|
||||
ConvertRGBA8888ToRAW8((u8*)frame->pixels, raw8_buffer1.data(), c_width, c_height);
|
||||
frame_data->pFramePointerList[0][0] = raw8_buffer1.data();
|
||||
break;
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
switch (output_config1.format.formatLevel0) {
|
||||
case ORBIS_CAMERA_FORMAT_YUV422:
|
||||
frame_data->pFramePointerList[1][0] = frame->pixels;
|
||||
break;
|
||||
case ORBIS_CAMERA_FORMAT_RAW16:
|
||||
ConvertRGBA8888ToRAW16((u8*)frame->pixels, raw16_buffer2.data(), c_width, c_height);
|
||||
frame_data->pFramePointerList[1][0] = raw16_buffer2.data();
|
||||
break;
|
||||
case ORBIS_CAMERA_FORMAT_RAW8:
|
||||
ConvertRGBA8888ToRAW8((u8*)frame->pixels, raw8_buffer2.data(), c_width, c_height);
|
||||
frame_data->pFramePointerList[1][0] = raw8_buffer2.data();
|
||||
break;
|
||||
default:
|
||||
UNREACHABLE();
|
||||
}
|
||||
frame_data->meta.format[0][0] = output_config0.format.formatLevel0;
|
||||
frame_data->meta.format[1][0] = output_config1.format.formatLevel0;
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceCameraGetGamma(s32 handle, OrbisCameraChannel channel, OrbisCameraGamma* gamma,
|
||||
@ -499,7 +619,7 @@ s32 PS4_SYSV_ABI sceCameraIsAttached(s32 index) {
|
||||
return ORBIS_CAMERA_ERROR_PARAM;
|
||||
}
|
||||
// 0 = disconnected, 1 = connected
|
||||
return 0;
|
||||
return EmulatorSettings.GetCameraId() == -1 ? 0 : 1;
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceCameraIsConfigChangeDone() {
|
||||
@ -516,16 +636,16 @@ s32 PS4_SYSV_ABI sceCameraIsValidFrameData(s32 handle, OrbisCameraFrameData* fra
|
||||
return ORBIS_CAMERA_ERROR_NOT_OPEN;
|
||||
}
|
||||
|
||||
return ORBIS_OK;
|
||||
return 1; // valid
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceCameraOpen(Libraries::UserService::OrbisUserServiceUserId user_id, s32 type,
|
||||
s32 index, OrbisCameraOpenParameter* param) {
|
||||
LOG_INFO(Lib_Camera, "called");
|
||||
if (user_id != Libraries::UserService::ORBIS_USER_SERVICE_USER_ID_SYSTEM || type != 0 ||
|
||||
index != 0) {
|
||||
return ORBIS_CAMERA_ERROR_PARAM;
|
||||
}
|
||||
LOG_WARNING(Lib_Camera, "Cameras are not supported yet");
|
||||
|
||||
g_library_opened = true;
|
||||
return ++g_handles;
|
||||
@ -609,15 +729,44 @@ s32 PS4_SYSV_ABI sceCameraSetCalibData() {
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceCameraSetConfig(s32 handle, OrbisCameraConfig* config) {
|
||||
LOG_DEBUG(Lib_Camera, "called");
|
||||
LOG_INFO(Lib_Camera, "called");
|
||||
if (handle < 1 || config == nullptr || config->sizeThis != sizeof(OrbisCameraConfig)) {
|
||||
return ORBIS_CAMERA_ERROR_PARAM;
|
||||
}
|
||||
if (!g_library_opened) {
|
||||
return ORBIS_CAMERA_ERROR_NOT_OPEN;
|
||||
}
|
||||
if (EmulatorSettings.GetCameraId() == -1) {
|
||||
return ORBIS_CAMERA_ERROR_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
return ORBIS_CAMERA_ERROR_NOT_CONNECTED;
|
||||
switch (config->configType) {
|
||||
case ORBIS_CAMERA_CONFIG_TYPE1:
|
||||
case ORBIS_CAMERA_CONFIG_TYPE2:
|
||||
case ORBIS_CAMERA_CONFIG_TYPE3:
|
||||
case ORBIS_CAMERA_CONFIG_TYPE4:
|
||||
output_config0 = camera_config_types[config->configType - 1][0];
|
||||
output_config1 = camera_config_types[config->configType - 1][1];
|
||||
break;
|
||||
case ORBIS_CAMERA_CONFIG_TYPE5:
|
||||
int sdk_ver;
|
||||
Libraries::Kernel::sceKernelGetCompiledSdkVersion(&sdk_ver);
|
||||
if (sdk_ver < Common::ElfInfo::FW_45) {
|
||||
return ORBIS_CAMERA_ERROR_UNKNOWN_CONFIG;
|
||||
}
|
||||
output_config0 = camera_config_types[config->configType - 1][0];
|
||||
output_config1 = camera_config_types[config->configType - 1][1];
|
||||
break;
|
||||
case ORBIS_CAMERA_CONFIG_EXTENTION:
|
||||
output_config0 = config->configExtention[0];
|
||||
output_config1 = config->configExtention[1];
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR(Lib_Camera, "Invalid config type {}", std::to_underlying(config->configType));
|
||||
return ORBIS_CAMERA_ERROR_PARAM;
|
||||
}
|
||||
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceCameraSetConfigInternal(s32 handle, OrbisCameraConfig* config) {
|
||||
@ -851,7 +1000,7 @@ s32 PS4_SYSV_ABI sceCameraSetWhiteBalance(s32 handle, OrbisCameraChannel channel
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceCameraStart(s32 handle, OrbisCameraStartParameter* param) {
|
||||
LOG_DEBUG(Lib_Camera, "called");
|
||||
LOG_INFO(Lib_Camera, "called");
|
||||
if (handle < 1 || param == nullptr || param->sizeThis != sizeof(OrbisCameraStartParameter)) {
|
||||
return ORBIS_CAMERA_ERROR_PARAM;
|
||||
}
|
||||
@ -864,6 +1013,79 @@ s32 PS4_SYSV_ABI sceCameraStart(s32 handle, OrbisCameraStartParameter* param) {
|
||||
return ORBIS_CAMERA_ERROR_FORMAT_UNKNOWN;
|
||||
}
|
||||
|
||||
if (param->formatLevel[0] > 1 || param->formatLevel[1] > 1) {
|
||||
LOG_ERROR(Lib_Camera, "Downscaled image retrieval isn't supported yet!");
|
||||
}
|
||||
|
||||
SDL_CameraID* devices = NULL;
|
||||
int devcount = 0;
|
||||
devices = SDL_GetCameras(&devcount);
|
||||
if (devices == NULL) {
|
||||
LOG_ERROR(Lib_Camera, "Couldn't enumerate camera devices: {}", SDL_GetError());
|
||||
return ORBIS_CAMERA_ERROR_FATAL;
|
||||
} else if (devcount == 0) {
|
||||
LOG_INFO(Lib_Camera, "No camera devices connected");
|
||||
return ORBIS_CAMERA_ERROR_NOT_CONNECTED;
|
||||
}
|
||||
raw8_buffer1.resize(c_width * c_height);
|
||||
raw16_buffer1.resize(c_width * c_height);
|
||||
raw8_buffer2.resize(c_width * c_height);
|
||||
raw16_buffer2.resize(c_width * c_height);
|
||||
SDL_CameraSpec cam_spec{};
|
||||
switch (output_config0.format.formatLevel0) {
|
||||
case ORBIS_CAMERA_FORMAT_YUV422:
|
||||
cam_spec.format = SDL_PIXELFORMAT_YUY2;
|
||||
break;
|
||||
case ORBIS_CAMERA_FORMAT_RAW8:
|
||||
cam_spec.format = SDL_PIXELFORMAT_RGBA8888; // to be swizzled
|
||||
break;
|
||||
case ORBIS_CAMERA_FORMAT_RAW16:
|
||||
cam_spec.format = SDL_PIXELFORMAT_RGBA8888; // to be swizzled
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG_ERROR(Lib_Camera, "Invalid format {}",
|
||||
std::to_underlying(output_config0.format.formatLevel0));
|
||||
break;
|
||||
}
|
||||
cam_spec.height = c_height;
|
||||
cam_spec.width = c_width;
|
||||
cam_spec.framerate_numerator = 60;
|
||||
cam_spec.framerate_denominator = 1;
|
||||
sdl_camera = SDL_OpenCamera(devices[EmulatorSettings.GetCameraId()], &cam_spec);
|
||||
LOG_INFO(Lib_Camera, "SDL backend in use: {}", SDL_GetCurrentCameraDriver());
|
||||
char const* camera_name = SDL_GetCameraName(devices[EmulatorSettings.GetCameraId()]);
|
||||
if (camera_name)
|
||||
LOG_INFO(Lib_Camera, "SDL camera name: {}", camera_name);
|
||||
SDL_CameraSpec spec;
|
||||
SDL_GetCameraFormat(sdl_camera, &spec);
|
||||
LOG_INFO(Lib_Camera, "SDL camera format: {:#x}", std::to_underlying(spec.format));
|
||||
LOG_INFO(Lib_Camera, "SDL camera framerate: {}",
|
||||
(float)spec.framerate_numerator / (float)spec.framerate_denominator);
|
||||
LOG_INFO(Lib_Camera, "SDL camera dimensions: {}x{}", spec.width, spec.height);
|
||||
|
||||
SDL_free(devices);
|
||||
|
||||
// "warm up" the device, as recommended by SDL
|
||||
u64 timestamp;
|
||||
SDL_Surface* frame = nullptr;
|
||||
frame = SDL_AcquireCameraFrame(sdl_camera, ×tamp);
|
||||
if (!frame) {
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
frame = SDL_AcquireCameraFrame(sdl_camera, ×tamp);
|
||||
if (frame) {
|
||||
SDL_ReleaseCameraFrame(sdl_camera, frame);
|
||||
break;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::nanoseconds(10));
|
||||
}
|
||||
}
|
||||
|
||||
if (!sdl_camera) {
|
||||
LOG_ERROR(Lib_Camera, "Failed to open camera: {}", SDL_GetError());
|
||||
return ORBIS_CAMERA_ERROR_FATAL;
|
||||
}
|
||||
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
|
||||
@ -102,6 +102,123 @@ struct OrbisCameraConfig {
|
||||
OrbisCameraConfigExtention configExtention[ORBIS_CAMERA_MAX_DEVICE_NUM];
|
||||
};
|
||||
|
||||
constexpr OrbisCameraConfigExtention camera_config_types[5][ORBIS_CAMERA_MAX_DEVICE_NUM]{
|
||||
{
|
||||
// type 1
|
||||
{
|
||||
.format =
|
||||
{
|
||||
.formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422,
|
||||
.formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_Y8,
|
||||
.formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_Y8,
|
||||
.formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_Y8,
|
||||
},
|
||||
.framerate = ORBIS_CAMERA_FRAMERATE_60,
|
||||
},
|
||||
{
|
||||
.format =
|
||||
{
|
||||
.formatLevel0 = ORBIS_CAMERA_FORMAT_RAW16,
|
||||
.formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_Y8,
|
||||
.formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_Y8,
|
||||
.formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_Y8,
|
||||
},
|
||||
.framerate = ORBIS_CAMERA_FRAMERATE_60,
|
||||
},
|
||||
},
|
||||
{
|
||||
// type 2
|
||||
{
|
||||
.format =
|
||||
{
|
||||
.formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422,
|
||||
.formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
.formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
.formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
},
|
||||
.framerate = ORBIS_CAMERA_FRAMERATE_60,
|
||||
},
|
||||
{
|
||||
.format =
|
||||
{
|
||||
.formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422,
|
||||
.formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
.formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
.formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
},
|
||||
.framerate = ORBIS_CAMERA_FRAMERATE_60,
|
||||
},
|
||||
},
|
||||
{
|
||||
// type 3
|
||||
{
|
||||
.format =
|
||||
{
|
||||
.formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422,
|
||||
.formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_Y8,
|
||||
.formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_Y8,
|
||||
.formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_Y8,
|
||||
},
|
||||
.framerate = ORBIS_CAMERA_FRAMERATE_60,
|
||||
},
|
||||
{
|
||||
.format =
|
||||
{
|
||||
.formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422,
|
||||
.formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_Y8,
|
||||
.formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_Y8,
|
||||
.formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_Y8,
|
||||
},
|
||||
.framerate = ORBIS_CAMERA_FRAMERATE_60,
|
||||
},
|
||||
},
|
||||
{
|
||||
// type 4
|
||||
{
|
||||
.format =
|
||||
{
|
||||
.formatLevel0 = ORBIS_CAMERA_FORMAT_RAW16,
|
||||
.formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
.formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
.formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
},
|
||||
.framerate = ORBIS_CAMERA_FRAMERATE_60,
|
||||
},
|
||||
{
|
||||
.format =
|
||||
{
|
||||
.formatLevel0 = ORBIS_CAMERA_FORMAT_RAW16,
|
||||
.formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
.formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
.formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
},
|
||||
.framerate = ORBIS_CAMERA_FRAMERATE_60,
|
||||
},
|
||||
},
|
||||
{
|
||||
// type 5
|
||||
{
|
||||
.format =
|
||||
{
|
||||
.formatLevel0 = ORBIS_CAMERA_FORMAT_YUV422,
|
||||
.formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
.formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
.formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
},
|
||||
.framerate = ORBIS_CAMERA_FRAMERATE_60,
|
||||
},
|
||||
{
|
||||
.format =
|
||||
{
|
||||
.formatLevel0 = ORBIS_CAMERA_FORMAT_RAW16,
|
||||
.formatLevel1 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
.formatLevel2 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
.formatLevel3 = ORBIS_CAMERA_SCALE_FORMAT_YUV422,
|
||||
},
|
||||
.framerate = ORBIS_CAMERA_FRAMERATE_60,
|
||||
},
|
||||
}};
|
||||
|
||||
enum OrbisCameraAecAgcTarget {
|
||||
ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_DEF = 0x00,
|
||||
ORBIS_CAMERA_ATTRIBUTE_AECAGC_TARGET_2_0 = 0x20,
|
||||
|
||||
@ -288,6 +288,9 @@ WindowSDL::WindowSDL(s32 width_, s32 height_, Input::GameController* controller_
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
UNREACHABLE_MSG("Failed to initialize SDL video subsystem: {}", SDL_GetError());
|
||||
}
|
||||
if (!SDL_Init(SDL_INIT_CAMERA)) {
|
||||
UNREACHABLE_MSG("Failed to initialize SDL camera subsystem: {}", SDL_GetError());
|
||||
}
|
||||
SDL_InitSubSystem(SDL_INIT_AUDIO);
|
||||
|
||||
SDL_PropertiesID props = SDL_CreateProperties();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user