obs-studio/plugins/linux-capture/xcompcap-main.cpp
Chris Angelico fd668695db linux-capture: Fix cursor draw bug when cropping window cap.
When cropping the left or top of a window capture, OBS would misalign
the drawn cursor, placing it as if the entire window were being
captured. Instead, offset the captured cursor by the same number of
pixels, thus placing the cursor back where it belongs.
2019-11-20 22:09:47 -08:00

688 lines
16 KiB
C++

#include <glad/glad.h>
#include <glad/glad_glx.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xcomposite.h>
#include <pthread.h>
#include <vector>
#include <obs-module.h>
#include <graphics/vec4.h>
#include <util/platform.h>
#include "xcompcap-main.hpp"
#include "xcompcap-helper.hpp"
#include "xcursor.h"
#define xdisp (XCompcap::disp())
#define WIN_STRING_DIV "\r\n"
bool XCompcapMain::init()
{
if (!xdisp) {
blog(LOG_ERROR, "failed opening display");
return false;
}
int eventBase, errorBase;
if (!XCompositeQueryExtension(xdisp, &eventBase, &errorBase)) {
blog(LOG_ERROR, "Xcomposite extension not supported");
return false;
}
int major = 0, minor = 2;
XCompositeQueryVersion(xdisp, &major, &minor);
if (major == 0 && minor < 2) {
blog(LOG_ERROR, "Xcomposite extension is too old: %d.%d < 0.2",
major, minor);
return false;
}
return true;
}
void XCompcapMain::deinit()
{
XCompcap::cleanupDisplay();
}
obs_properties_t *XCompcapMain::properties()
{
obs_properties_t *props = obs_properties_create();
obs_property_t *wins = obs_properties_add_list(
props, "capture_window", obs_module_text("Window"),
OBS_COMBO_TYPE_LIST, OBS_COMBO_FORMAT_STRING);
for (Window win : XCompcap::getTopLevelWindows()) {
std::string wname = XCompcap::getWindowName(win);
std::string cls = XCompcap::getWindowClass(win);
std::string winid = std::to_string((long long)win);
std::string desc =
(winid + WIN_STRING_DIV + wname + WIN_STRING_DIV + cls);
obs_property_list_add_string(wins, wname.c_str(), desc.c_str());
}
obs_properties_add_int(props, "cut_top", obs_module_text("CropTop"), 0,
4096, 1);
obs_properties_add_int(props, "cut_left", obs_module_text("CropLeft"),
0, 4096, 1);
obs_properties_add_int(props, "cut_right", obs_module_text("CropRight"),
0, 4096, 1);
obs_properties_add_int(props, "cut_bot", obs_module_text("CropBottom"),
0, 4096, 1);
obs_properties_add_bool(props, "swap_redblue",
obs_module_text("SwapRedBlue"));
obs_properties_add_bool(props, "lock_x", obs_module_text("LockX"));
obs_properties_add_bool(props, "show_cursor",
obs_module_text("CaptureCursor"));
obs_properties_add_bool(props, "include_border",
obs_module_text("IncludeXBorder"));
obs_properties_add_bool(props, "exclude_alpha",
obs_module_text("ExcludeAlpha"));
return props;
}
void XCompcapMain::defaults(obs_data_t *settings)
{
obs_data_set_default_string(settings, "capture_window", "");
obs_data_set_default_int(settings, "cut_top", 0);
obs_data_set_default_int(settings, "cut_left", 0);
obs_data_set_default_int(settings, "cut_right", 0);
obs_data_set_default_int(settings, "cut_bot", 0);
obs_data_set_default_bool(settings, "swap_redblue", false);
obs_data_set_default_bool(settings, "lock_x", false);
obs_data_set_default_bool(settings, "show_cursor", true);
obs_data_set_default_bool(settings, "include_border", false);
obs_data_set_default_bool(settings, "exclude_alpha", false);
}
#define FIND_WINDOW_INTERVAL 2.0
struct XCompcapMain_private {
XCompcapMain_private()
: win(0),
cut_top(0),
cur_cut_top(0),
cut_left(0),
cur_cut_left(0),
cut_right(0),
cur_cut_right(0),
cut_bot(0),
cur_cut_bot(0),
inverted(false),
width(0),
height(0),
pixmap(0),
glxpixmap(0),
tex(0),
gltex(0)
{
pthread_mutexattr_init(&lockattr);
pthread_mutexattr_settype(&lockattr, PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock, &lockattr);
}
~XCompcapMain_private()
{
pthread_mutex_destroy(&lock);
pthread_mutexattr_destroy(&lockattr);
}
obs_source_t *source;
std::string windowName;
Window win = 0;
int cut_top, cur_cut_top;
int cut_left, cur_cut_left;
int cut_right, cur_cut_right;
int cut_bot, cur_cut_bot;
bool inverted;
bool swapRedBlue;
bool lockX;
bool include_border;
bool exclude_alpha;
bool draw_opaque;
double window_check_time = 0.0;
uint32_t width;
uint32_t height;
uint32_t border;
Pixmap pixmap;
GLXPixmap glxpixmap;
gs_texture_t *tex;
gs_texture_t *gltex;
pthread_mutex_t lock;
pthread_mutexattr_t lockattr;
bool show_cursor = true;
bool cursor_outside = false;
xcursor_t *cursor = nullptr;
};
XCompcapMain::XCompcapMain(obs_data_t *settings, obs_source_t *source)
{
p = new XCompcapMain_private;
p->source = source;
obs_enter_graphics();
p->cursor = xcursor_init(xdisp);
obs_leave_graphics();
updateSettings(settings);
}
static void xcc_cleanup(XCompcapMain_private *p);
XCompcapMain::~XCompcapMain()
{
ObsGsContextHolder obsctx;
if (p->tex) {
gs_texture_destroy(p->tex);
p->tex = 0;
}
xcc_cleanup(p);
if (p->cursor) {
xcursor_destroy(p->cursor);
p->cursor = nullptr;
}
delete p;
}
static Window getWindowFromString(std::string wstr)
{
XErrorLock xlock;
if (wstr == "") {
return XCompcap::getTopLevelWindows().front();
}
if (wstr.substr(0, 4) == "root") {
int i = std::stoi("0" + wstr.substr(4));
return RootWindow(xdisp, i);
}
size_t firstMark = wstr.find(WIN_STRING_DIV);
size_t markSize = strlen(WIN_STRING_DIV);
if (firstMark == std::string::npos)
return (Window)std::stol(wstr);
Window wid = 0;
wstr = wstr.substr(firstMark + markSize);
size_t lastMark = wstr.rfind(WIN_STRING_DIV);
std::string wname = wstr.substr(0, lastMark);
std::string wcls = wstr.substr(lastMark + markSize);
Window matchedNameWin = wid;
for (Window cwin : XCompcap::getTopLevelWindows()) {
std::string cwinname = XCompcap::getWindowName(cwin);
std::string ccls = XCompcap::getWindowClass(cwin);
if (cwin == wid && wname == cwinname && wcls == ccls)
return wid;
if (wname == cwinname ||
(!matchedNameWin && !wcls.empty() && wcls == ccls))
matchedNameWin = cwin;
}
return matchedNameWin;
}
static void xcc_cleanup(XCompcapMain_private *p)
{
PLock lock(&p->lock);
XErrorLock xlock;
if (p->gltex) {
GLuint gltex = *(GLuint *)gs_texture_get_obj(p->gltex);
glBindTexture(GL_TEXTURE_2D, gltex);
if (p->glxpixmap) {
glXReleaseTexImageEXT(xdisp, p->glxpixmap,
GLX_FRONT_LEFT_EXT);
if (xlock.gotError()) {
blog(LOG_ERROR,
"cleanup glXReleaseTexImageEXT failed: %s",
xlock.getErrorText().c_str());
xlock.resetError();
}
glXDestroyPixmap(xdisp, p->glxpixmap);
if (xlock.gotError()) {
blog(LOG_ERROR,
"cleanup glXDestroyPixmap failed: %s",
xlock.getErrorText().c_str());
xlock.resetError();
}
p->glxpixmap = 0;
}
gs_texture_destroy(p->gltex);
p->gltex = 0;
}
if (p->pixmap) {
XFreePixmap(xdisp, p->pixmap);
if (xlock.gotError()) {
blog(LOG_ERROR, "cleanup glXDestroyPixmap failed: %s",
xlock.getErrorText().c_str());
xlock.resetError();
}
p->pixmap = 0;
}
if (p->win) {
XCompositeUnredirectWindow(xdisp, p->win,
CompositeRedirectAutomatic);
XSelectInput(xdisp, p->win, 0);
p->win = 0;
}
if (p->tex) {
gs_texture_destroy(p->tex);
p->tex = 0;
}
}
static gs_color_format gs_format_from_tex()
{
GLint iformat = 0;
// consider GL_ARB_internalformat_query
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT,
&iformat);
// These formats are known to be wrong on Intel platforms. We intentionally
// use swapped internal formats here to preserve historic behavior which
// swapped colors accidentally and because D3D11 would not support a
// GS_RGBX format
switch (iformat) {
case GL_RGB:
return GS_BGRX;
case GL_RGBA:
return GS_RGBA;
default:
return GS_RGBA;
}
}
// from libobs-opengl/gl-subsystem.h because we need to handle GLX modifying textures outside libobs.
struct fb_info;
struct gs_texture {
gs_device_t *device;
enum gs_texture_type type;
enum gs_color_format format;
GLenum gl_format;
GLenum gl_target;
GLenum gl_internal_format;
GLenum gl_type;
GLuint texture;
uint32_t levels;
bool is_dynamic;
bool is_render_target;
bool is_dummy;
bool gen_mipmaps;
gs_samplerstate_t *cur_sampler;
struct fbo_info *fbo;
};
// End shitty hack.
void XCompcapMain::updateSettings(obs_data_t *settings)
{
PLock lock(&p->lock);
ObsGsContextHolder obsctx;
blog(LOG_DEBUG, "Settings updating");
Window prevWin = p->win;
xcc_cleanup(p);
if (settings) {
const char *windowName =
obs_data_get_string(settings, "capture_window");
p->windowName = windowName;
p->win = getWindowFromString(windowName);
p->cut_top = obs_data_get_int(settings, "cut_top");
p->cut_left = obs_data_get_int(settings, "cut_left");
p->cut_right = obs_data_get_int(settings, "cut_right");
p->cut_bot = obs_data_get_int(settings, "cut_bot");
p->lockX = obs_data_get_bool(settings, "lock_x");
p->swapRedBlue = obs_data_get_bool(settings, "swap_redblue");
p->show_cursor = obs_data_get_bool(settings, "show_cursor");
p->include_border =
obs_data_get_bool(settings, "include_border");
p->exclude_alpha = obs_data_get_bool(settings, "exclude_alpha");
p->draw_opaque = false;
} else {
p->win = prevWin;
}
XErrorLock xlock;
if (p->win)
XCompositeRedirectWindow(xdisp, p->win,
CompositeRedirectAutomatic);
if (xlock.gotError()) {
blog(LOG_ERROR, "XCompositeRedirectWindow failed: %s",
xlock.getErrorText().c_str());
return;
}
if (p->win)
XSelectInput(xdisp, p->win,
StructureNotifyMask | ExposureMask |
VisibilityChangeMask);
XSync(xdisp, 0);
XWindowAttributes attr;
if (!p->win || !XGetWindowAttributes(xdisp, p->win, &attr)) {
p->win = 0;
p->width = 0;
p->height = 0;
return;
}
if (p->win && p->cursor && p->show_cursor) {
Window child;
int x, y;
XTranslateCoordinates(xdisp, p->win, attr.root, 0, 0, &x, &y,
&child);
xcursor_offset(p->cursor, x, y);
}
const int config_attrs[] = {GLX_BIND_TO_TEXTURE_RGBA_EXT,
GL_TRUE,
GLX_DRAWABLE_TYPE,
GLX_PIXMAP_BIT,
GLX_BIND_TO_TEXTURE_TARGETS_EXT,
GLX_TEXTURE_2D_BIT_EXT,
GLX_DOUBLEBUFFER,
GL_FALSE,
None};
int nelem = 0;
GLXFBConfig *configs = glXChooseFBConfig(
xdisp, XCompcap::getRootWindowScreen(attr.root), config_attrs,
&nelem);
bool found = false;
GLXFBConfig config;
for (int i = 0; i < nelem; i++) {
config = configs[i];
XVisualInfo *visual = glXGetVisualFromFBConfig(xdisp, config);
if (!visual)
continue;
if (attr.depth != visual->depth) {
XFree(visual);
continue;
}
XFree(visual);
found = true;
break;
}
if (!found) {
blog(LOG_ERROR, "no matching fb config found");
p->win = 0;
p->height = 0;
p->width = 0;
XFree(configs);
return;
}
if (p->exclude_alpha || attr.depth != 32) {
p->draw_opaque = true;
}
int inverted;
glXGetFBConfigAttrib(xdisp, config, GLX_Y_INVERTED_EXT, &inverted);
p->inverted = inverted != 0;
p->border = attr.border_width;
if (p->include_border) {
p->width = attr.width + p->border * 2;
p->height = attr.height + p->border * 2;
} else {
p->width = attr.width;
p->height = attr.height;
}
if (p->cut_top + p->cut_bot < (int)p->height) {
p->cur_cut_top = p->cut_top;
p->cur_cut_bot = p->cut_bot;
} else {
p->cur_cut_top = 0;
p->cur_cut_bot = 0;
}
if (p->cut_left + p->cut_right < (int)p->width) {
p->cur_cut_left = p->cut_left;
p->cur_cut_right = p->cut_right;
} else {
p->cur_cut_left = 0;
p->cur_cut_right = 0;
}
// Precautionary since we dont error check every GLX call above.
xlock.resetError();
p->pixmap = XCompositeNameWindowPixmap(xdisp, p->win);
if (xlock.gotError()) {
blog(LOG_ERROR, "XCompositeNameWindowPixmap failed: %s",
xlock.getErrorText().c_str());
p->pixmap = 0;
XFree(configs);
return;
}
// Should be consistent format with config we are using. Since we searched on RGBA lets use RGBA here.
const int pixmap_attrs[] = {GLX_TEXTURE_TARGET_EXT, GLX_TEXTURE_2D_EXT,
GLX_TEXTURE_FORMAT_EXT,
GLX_TEXTURE_FORMAT_RGBA_EXT, None};
p->glxpixmap = glXCreatePixmap(xdisp, config, p->pixmap, pixmap_attrs);
if (xlock.gotError()) {
blog(LOG_ERROR, "glXCreatePixmap failed: %s",
xlock.getErrorText().c_str());
XFreePixmap(xdisp, p->pixmap);
XFree(configs);
p->pixmap = 0;
p->glxpixmap = 0;
return;
}
XFree(configs);
// Build an OBS texture to bind the pixmap to.
p->gltex = gs_texture_create(p->width, p->height, GS_RGBA, 1, 0,
GS_GL_DUMMYTEX);
GLuint gltex = *(GLuint *)gs_texture_get_obj(p->gltex);
glBindTexture(GL_TEXTURE_2D, gltex);
glXBindTexImageEXT(xdisp, p->glxpixmap, GLX_FRONT_LEFT_EXT, NULL);
if (xlock.gotError()) {
blog(LOG_ERROR, "glXBindTexImageEXT failed: %s",
xlock.getErrorText().c_str());
XFreePixmap(xdisp, p->pixmap);
XFree(configs);
p->pixmap = 0;
p->glxpixmap = 0;
return;
}
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// glxBindTexImageEXT might modify the textures format.
gs_color_format format = gs_format_from_tex();
glBindTexture(GL_TEXTURE_2D, 0);
// sync OBS texture format based on any glxBindTexImageEXT changes
p->gltex->format = format;
// Create a pure OBS texture to use for rendering. Using the same
// format so we can copy instead of drawing from the source gltex.
if (p->tex)
gs_texture_destroy(p->tex);
p->tex = gs_texture_create(width(), height(), format, 1, 0,
GS_GL_DUMMYTEX);
if (p->swapRedBlue) {
GLuint tex = *(GLuint *)gs_texture_get_obj(p->tex);
glBindTexture(GL_TEXTURE_2D, tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, GL_RED);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, GL_BLUE);
glBindTexture(GL_TEXTURE_2D, 0);
}
if (!p->windowName.empty()) {
blog(LOG_INFO,
"[window-capture: '%s'] update settings:\n"
"\ttitle: %s\n"
"\tclass: %s\n"
"\tBit depth: %i\n"
"\tFound proper GLXFBConfig (in %i): %s\n",
obs_source_get_name(p->source),
XCompcap::getWindowName(p->win).c_str(),
XCompcap::getWindowClass(p->win).c_str(), attr.depth,
nelem, found ? "yes" : "no");
}
}
void XCompcapMain::tick(float seconds)
{
if (!obs_source_showing(p->source))
return;
PLock lock(&p->lock, true);
if (!lock.isLocked())
return;
XCompcap::processEvents();
if (p->win && XCompcap::windowWasReconfigured(p->win)) {
p->window_check_time = FIND_WINDOW_INTERVAL;
p->win = 0;
}
XDisplayLock xlock;
XWindowAttributes attr;
if (!p->win || !XGetWindowAttributes(xdisp, p->win, &attr)) {
p->window_check_time += (double)seconds;
if (p->window_check_time < FIND_WINDOW_INTERVAL)
return;
Window newWin = getWindowFromString(p->windowName);
p->window_check_time = 0.0;
if (newWin && XGetWindowAttributes(xdisp, newWin, &attr)) {
p->win = newWin;
updateSettings(0);
} else {
return;
}
}
if (!p->tex || !p->gltex)
return;
obs_enter_graphics();
if (p->lockX) {
// XDisplayLock is still live so we should already be locked.
XLockDisplay(xdisp);
XSync(xdisp, 0);
}
if (p->include_border) {
gs_copy_texture_region(p->tex, 0, 0, p->gltex, p->cur_cut_left,
p->cur_cut_top, width(), height());
} else {
gs_copy_texture_region(p->tex, 0, 0, p->gltex,
p->cur_cut_left + p->border,
p->cur_cut_top + p->border, width(),
height());
}
if (p->cursor && p->show_cursor) {
xcursor_tick(p->cursor);
p->cursor_outside =
p->cursor->x < p->cur_cut_left ||
p->cursor->y < p->cur_cut_top ||
p->cursor->x > int(p->width - p->cur_cut_right) ||
p->cursor->y > int(p->height - p->cur_cut_bot);
}
if (p->lockX)
XUnlockDisplay(xdisp);
obs_leave_graphics();
}
void XCompcapMain::render(gs_effect_t *effect)
{
if (!p->win)
return;
PLock lock(&p->lock, true);
if (p->draw_opaque)
effect = obs_get_base_effect(OBS_EFFECT_OPAQUE);
else
effect = obs_get_base_effect(OBS_EFFECT_DEFAULT);
if (!lock.isLocked() || !p->tex)
return;
gs_eparam_t *image = gs_effect_get_param_by_name(effect, "image");
gs_effect_set_texture(image, p->tex);
while (gs_effect_loop(effect, "Draw")) {
gs_draw_sprite(p->tex, 0, 0, 0);
}
if (p->cursor && p->gltex && p->show_cursor && !p->cursor_outside) {
effect = obs_get_base_effect(OBS_EFFECT_DEFAULT);
while (gs_effect_loop(effect, "Draw")) {
xcursor_render(p->cursor, -p->cur_cut_left,
-p->cur_cut_top);
}
}
}
uint32_t XCompcapMain::width()
{
if (!p->win)
return 0;
return p->width - p->cur_cut_left - p->cur_cut_right;
}
uint32_t XCompcapMain::height()
{
if (!p->win)
return 0;
return p->height - p->cur_cut_bot - p->cur_cut_top;
}