summaryrefslogtreecommitdiff
path: root/src/managers
diff options
context:
space:
mode:
authorIkalco <73481042+ikalco@users.noreply.github.com>2026-02-22 06:30:11 -0600
committerGitHub <noreply@github.com>2026-02-22 12:30:11 +0000
commitb4ee4674f9a74e3d602c7fb17bc09f79d221583c (patch)
tree294bbb207c44e778dd78afda65cbd79181f04124 /src/managers
parent93dbf884261efcb387e920b1cd0e4ed5b288bc3a (diff)
protocols: implement image-capture-source-v1 and image-copy-capture-v1 (#11709)
Implements the new screencopy protocols
Diffstat (limited to 'src/managers')
-rw-r--r--src/managers/PointerManager.cpp15
-rw-r--r--src/managers/PointerManager.hpp47
-rw-r--r--src/managers/ProtocolManager.cpp12
-rw-r--r--src/managers/permissions/DynamicPermissionManager.cpp2
-rw-r--r--src/managers/permissions/DynamicPermissionManager.hpp3
-rw-r--r--src/managers/screenshare/CursorshareSession.cpp240
-rw-r--r--src/managers/screenshare/ScreenshareFrame.cpp493
-rw-r--r--src/managers/screenshare/ScreenshareManager.cpp161
-rw-r--r--src/managers/screenshare/ScreenshareManager.hpp252
-rw-r--r--src/managers/screenshare/ScreenshareSession.cpp162
10 files changed, 1365 insertions, 22 deletions
diff --git a/src/managers/PointerManager.cpp b/src/managers/PointerManager.cpp
index 2d752ea7..60964d4d 100644
--- a/src/managers/PointerManager.cpp
+++ b/src/managers/PointerManager.cpp
@@ -96,6 +96,10 @@ Vector2D CPointerManager::position() {
return m_pointerPos;
}
+Vector2D CPointerManager::hotspot() {
+ return m_currentCursorImage.hotspot;
+}
+
bool CPointerManager::hasCursor() {
return m_currentCursorImage.pBuffer || m_currentCursorImage.surface;
}
@@ -115,6 +119,7 @@ void CPointerManager::setCursorBuffer(SP<Aquamarine::IBuffer> buf, const Vector2
m_currentCursorImage.scale = scale;
updateCursorBackend();
damageIfSoftware();
+ m_events.cursorChanged.emit();
}
return;
@@ -132,6 +137,7 @@ void CPointerManager::setCursorBuffer(SP<Aquamarine::IBuffer> buf, const Vector2
updateCursorBackend();
damageIfSoftware();
+ m_events.cursorChanged.emit();
}
void CPointerManager::setCursorSurface(SP<Desktop::View::CWLSurface> surf, const Vector2D& hotspot) {
@@ -143,6 +149,7 @@ void CPointerManager::setCursorSurface(SP<Desktop::View::CWLSurface> surf, const
m_currentCursorImage.scale = surf && surf->resource() ? surf->resource()->m_current.scale : 1.F;
updateCursorBackend();
damageIfSoftware();
+ m_events.cursorChanged.emit();
}
return;
@@ -164,6 +171,7 @@ void CPointerManager::setCursorSurface(SP<Desktop::View::CWLSurface> surf, const
recheckEnteredOutputs();
updateCursorBackend();
damageIfSoftware();
+ m_events.cursorChanged.emit();
});
if (surf->resource()->m_current.texture) {
@@ -177,6 +185,7 @@ void CPointerManager::setCursorSurface(SP<Desktop::View::CWLSurface> surf, const
recheckEnteredOutputs();
updateCursorBackend();
damageIfSoftware();
+ m_events.cursorChanged.emit();
}
void CPointerManager::recheckEnteredOutputs() {
@@ -261,6 +270,8 @@ void CPointerManager::resetCursorImage(bool apply) {
ms->cursorFrontBuffer = nullptr;
}
}
+
+ m_events.cursorChanged.emit();
}
void CPointerManager::updateCursorBackend() {
@@ -888,6 +899,10 @@ void CPointerManager::onMonitorLayoutChange() {
damageIfSoftware();
}
+const CPointerManager::SCursorImage& CPointerManager::currentCursorImage() {
+ return m_currentCursorImage;
+}
+
SP<CTexture> CPointerManager::getCurrentCursorTexture() {
if (!m_currentCursorImage.pBuffer && (!m_currentCursorImage.surface || !m_currentCursorImage.surface->resource()->m_current.texture))
return nullptr;
diff --git a/src/managers/PointerManager.hpp b/src/managers/PointerManager.hpp
index 4b8ec65a..0109268b 100644
--- a/src/managers/PointerManager.hpp
+++ b/src/managers/PointerManager.hpp
@@ -60,10 +60,34 @@ class CPointerManager {
//
Vector2D position();
+ Vector2D hotspot();
Vector2D cursorSizeLogical();
void recheckEnteredOutputs();
+ // returns the thing in global coords
+ CBox getCursorBoxGlobal();
+
+ struct SCursorImage {
+ SP<Aquamarine::IBuffer> pBuffer;
+ SP<CTexture> bufferTex;
+ WP<Desktop::View::CWLSurface> surface;
+
+ Vector2D hotspot;
+ Vector2D size;
+ float scale = 1.F;
+
+ CHyprSignalListener destroySurface;
+ CHyprSignalListener commitSurface;
+ };
+
+ const SCursorImage& currentCursorImage();
+ SP<CTexture> getCurrentCursorTexture();
+
+ struct {
+ CSignalT<> cursorChanged;
+ } m_events;
+
private:
void recheckPointerPosition();
void onMonitorLayoutChange();
@@ -79,13 +103,9 @@ class CPointerManager {
// returns the thing in device coordinates. Is NOT offset by the hotspot, relies on set_cursor with hotspot.
Vector2D getCursorPosForMonitor(PHLMONITOR pMonitor);
// returns the thing in logical coordinates of the monitor
- CBox getCursorBoxLogicalForMonitor(PHLMONITOR pMonitor);
- // returns the thing in global coords
- CBox getCursorBoxGlobal();
-
- Vector2D transformedHotspot(PHLMONITOR pMonitor);
+ CBox getCursorBoxLogicalForMonitor(PHLMONITOR pMonitor);
- SP<CTexture> getCurrentCursorTexture();
+ Vector2D transformedHotspot(PHLMONITOR pMonitor);
struct SPointerListener {
CHyprSignalListener destroy;
@@ -137,20 +157,9 @@ class CPointerManager {
std::vector<CBox> monitorBoxes;
} m_currentMonitorLayout;
- struct {
- SP<Aquamarine::IBuffer> pBuffer;
- SP<CTexture> bufferTex;
- WP<Desktop::View::CWLSurface> surface;
-
- Vector2D hotspot;
- Vector2D size;
- float scale = 1.F;
-
- CHyprSignalListener destroySurface;
- CHyprSignalListener commitSurface;
- } m_currentCursorImage; // TODO: support various sizes per-output so we can have pixel-perfect cursors
+ SCursorImage m_currentCursorImage; // TODO: support various sizes per-output so we can have pixel-perfect cursors
- Vector2D m_pointerPos = {0, 0};
+ Vector2D m_pointerPos = {0, 0};
struct SMonitorPointerState {
SMonitorPointerState(const PHLMONITOR& m) : monitor(m) {}
diff --git a/src/managers/ProtocolManager.cpp b/src/managers/ProtocolManager.cpp
index 6376f2a0..216c07f1 100644
--- a/src/managers/ProtocolManager.cpp
+++ b/src/managers/ProtocolManager.cpp
@@ -50,6 +50,8 @@
#include "../protocols/SecurityContext.hpp"
#include "../protocols/CTMControl.hpp"
#include "../protocols/HyprlandSurface.hpp"
+#include "../protocols/ImageCaptureSource.hpp"
+#include "../protocols/ImageCopyCapture.hpp"
#include "../protocols/core/Seat.hpp"
#include "../protocols/core/DataDevice.hpp"
#include "../protocols/core/Compositor.hpp"
@@ -65,6 +67,7 @@
#include "../protocols/PointerWarp.hpp"
#include "../protocols/Fifo.hpp"
#include "../protocols/CommitTiming.hpp"
+#include "HookSystemManager.hpp"
#include "../helpers/Monitor.hpp"
#include "../render/Renderer.hpp"
@@ -180,8 +183,6 @@ CProtocolManager::CProtocolManager() {
PROTO::dataWlr = makeUnique<CDataDeviceWLRProtocol>(&zwlr_data_control_manager_v1_interface, 2, "DataDeviceWlr");
PROTO::primarySelection = makeUnique<CPrimarySelectionProtocol>(&zwp_primary_selection_device_manager_v1_interface, 1, "PrimarySelection");
PROTO::xwaylandShell = makeUnique<CXWaylandShellProtocol>(&xwayland_shell_v1_interface, 1, "XWaylandShell");
- PROTO::screencopy = makeUnique<CScreencopyProtocol>(&zwlr_screencopy_manager_v1_interface, 3, "Screencopy");
- PROTO::toplevelExport = makeUnique<CToplevelExportProtocol>(&hyprland_toplevel_export_manager_v1_interface, 2, "ToplevelExport");
PROTO::toplevelMapping = makeUnique<CToplevelMappingProtocol>(&hyprland_toplevel_mapping_manager_v1_interface, 1, "ToplevelMapping");
PROTO::globalShortcuts = makeUnique<CGlobalShortcutsProtocol>(&hyprland_global_shortcuts_manager_v1_interface, 1, "GlobalShortcuts");
PROTO::xdgDialog = makeUnique<CXDGDialogProtocol>(&xdg_wm_dialog_v1_interface, 1, "XDGDialog");
@@ -200,6 +201,12 @@ CProtocolManager::CProtocolManager() {
if (*PENABLECT)
PROTO::commitTiming = makeUnique<CCommitTimingProtocol>(&wp_commit_timing_manager_v1_interface, 1, "CommitTiming");
+ // Screensharing Protocols
+ PROTO::screencopy = makeUnique<CScreencopyProtocol>(&zwlr_screencopy_manager_v1_interface, 3, "Screencopy");
+ PROTO::toplevelExport = makeUnique<CToplevelExportProtocol>(&hyprland_toplevel_export_manager_v1_interface, 2, "ToplevelExport");
+ PROTO::imageCaptureSource = makeUnique<CImageCaptureSourceProtocol>(); // ctor inits actual protos, output and toplevel
+ PROTO::imageCopyCapture = makeUnique<CImageCopyCaptureProtocol>(&ext_image_copy_capture_manager_v1_interface, 1, "ImageCopyCapture");
+
if (*PENABLECM)
PROTO::colorManagement = makeUnique<CColorManagementProtocol>(&wp_color_manager_v1_interface, 1, "ColorManagement", *PDEBUGCM);
@@ -298,6 +305,7 @@ CProtocolManager::~CProtocolManager() {
PROTO::pointerWarp.reset();
PROTO::fifo.reset();
PROTO::commitTiming.reset();
+ PROTO::imageCaptureSource.reset();
for (auto& [_, lease] : PROTO::lease) {
lease.reset();
diff --git a/src/managers/permissions/DynamicPermissionManager.cpp b/src/managers/permissions/DynamicPermissionManager.cpp
index e2865459..d63a72a0 100644
--- a/src/managers/permissions/DynamicPermissionManager.cpp
+++ b/src/managers/permissions/DynamicPermissionManager.cpp
@@ -53,6 +53,7 @@ static const char* permissionToString(eDynamicPermissionType type) {
case PERMISSION_TYPE_SCREENCOPY: return "PERMISSION_TYPE_SCREENCOPY";
case PERMISSION_TYPE_PLUGIN: return "PERMISSION_TYPE_PLUGIN";
case PERMISSION_TYPE_KEYBOARD: return "PERMISSION_TYPE_KEYBOARD";
+ case PERMISSION_TYPE_CURSOR_POS: return "PERMISSION_TYPE_CURSOR_POS";
}
return "error";
@@ -251,6 +252,7 @@ void CDynamicPermissionManager::askForPermission(wl_client* client, const std::s
std::string description = "";
switch (rule->m_type) {
case PERMISSION_TYPE_SCREENCOPY: description = I18n::i18nEngine()->localize(I18n::TXT_KEY_PERMISSION_REQUEST_SCREENCOPY, {{"app", appName}}); break;
+ case PERMISSION_TYPE_CURSOR_POS: description = I18n::i18nEngine()->localize(I18n::TXT_KEY_PERMISSION_REQUEST_CURSOR_POS, {{"app", appName}}); break;
case PERMISSION_TYPE_PLUGIN: description = I18n::i18nEngine()->localize(I18n::TXT_KEY_PERMISSION_REQUEST_PLUGIN, {{"app", appName}, {"plugin", binaryPath}}); break;
case PERMISSION_TYPE_KEYBOARD: description = I18n::i18nEngine()->localize(I18n::TXT_KEY_PERMISSION_REQUEST_KEYBOARD, {{"keyboard", binaryPath}}); break;
case PERMISSION_TYPE_UNKNOWN: description = I18n::i18nEngine()->localize(I18n::TXT_KEY_PERMISSION_REQUEST_UNKNOWN, {{"app", appName}}); break;
diff --git a/src/managers/permissions/DynamicPermissionManager.hpp b/src/managers/permissions/DynamicPermissionManager.hpp
index 4de1eb32..423596c3 100644
--- a/src/managers/permissions/DynamicPermissionManager.hpp
+++ b/src/managers/permissions/DynamicPermissionManager.hpp
@@ -18,6 +18,7 @@ enum eDynamicPermissionType : uint8_t {
PERMISSION_TYPE_SCREENCOPY,
PERMISSION_TYPE_PLUGIN,
PERMISSION_TYPE_KEYBOARD,
+ PERMISSION_TYPE_CURSOR_POS,
};
enum eDynamicPermissionRuleSource : uint8_t {
@@ -104,4 +105,4 @@ class CDynamicPermissionManager {
std::vector<SP<CDynamicPermissionRule>> m_rules;
};
-inline UP<CDynamicPermissionManager> g_pDynamicPermissionManager; \ No newline at end of file
+inline UP<CDynamicPermissionManager> g_pDynamicPermissionManager;
diff --git a/src/managers/screenshare/CursorshareSession.cpp b/src/managers/screenshare/CursorshareSession.cpp
new file mode 100644
index 00000000..2322625f
--- /dev/null
+++ b/src/managers/screenshare/CursorshareSession.cpp
@@ -0,0 +1,240 @@
+#include "ScreenshareManager.hpp"
+#include "../PointerManager.hpp"
+#include "../../protocols/core/Seat.hpp"
+#include "../permissions/DynamicPermissionManager.hpp"
+#include "../../render/Renderer.hpp"
+
+using namespace Screenshare;
+
+CCursorshareSession::CCursorshareSession(wl_client* client, WP<CWLPointerResource> pointer) : m_client(client), m_pointer(pointer) {
+ m_listeners.pointerDestroyed = m_pointer->m_events.destroyed.listen([this] { stop(); });
+ m_listeners.cursorChanged = g_pPointerManager->m_events.cursorChanged.listen([this] {
+ calculateConstraints();
+ m_events.constraintsChanged.emit();
+
+ if (m_pendingFrame.pending) {
+ if (copy())
+ return;
+
+ LOGM(Log::ERR, "Failed to copy cursor image for cursor share");
+ if (m_pendingFrame.callback)
+ m_pendingFrame.callback(RESULT_NOT_COPIED);
+ m_pendingFrame.pending = false;
+ return;
+ }
+ });
+
+ calculateConstraints();
+}
+
+CCursorshareSession::~CCursorshareSession() {
+ stop();
+}
+
+void CCursorshareSession::stop() {
+ if (m_stopped)
+ return;
+ m_stopped = true;
+ m_events.stopped.emit();
+}
+
+void CCursorshareSession::calculateConstraints() {
+ const auto& cursorImage = g_pPointerManager->currentCursorImage();
+ m_constraintsChanged = true;
+
+ // cursor is hidden, keep the previous constraints and render 0 alpha
+ if (!cursorImage.pBuffer)
+ return;
+
+ // TODO: should cursor share have a format bit flip for RGBA?
+ if (auto attrs = cursorImage.pBuffer->shm(); attrs.success) {
+ m_format = attrs.format;
+ } else {
+ // we only have shm cursors
+ return;
+ }
+
+ m_hotspot = cursorImage.hotspot;
+ m_bufferSize = cursorImage.size;
+}
+
+// TODO: allow render to buffer without monitor and remove monitor param
+eScreenshareError CCursorshareSession::share(PHLMONITOR monitor, SP<IHLBuffer> buffer, FSourceBoxCallback sourceBoxCallback, FScreenshareCallback callback) {
+ if (m_stopped || m_pointer.expired() || m_bufferSize == Vector2D(0, 0))
+ return ERROR_STOPPED;
+
+ if UNLIKELY (!buffer || !buffer->m_resource || !buffer->m_resource->good()) {
+ LOGM(Log::ERR, "Client requested sharing to an invalid buffer");
+ return ERROR_NO_BUFFER;
+ }
+
+ if UNLIKELY (buffer->size != m_bufferSize) {
+ LOGM(Log::ERR, "Client requested sharing to an invalid buffer size");
+ return ERROR_BUFFER_SIZE;
+ }
+
+ uint32_t bufFormat;
+ if (buffer->dmabuf().success)
+ bufFormat = buffer->dmabuf().format;
+ else if (buffer->shm().success)
+ bufFormat = buffer->shm().format;
+ else {
+ LOGM(Log::ERR, "Client requested sharing to an invalid buffer");
+ return ERROR_NO_BUFFER;
+ }
+
+ if (bufFormat != m_format) {
+ LOGM(Log::ERR, "Invalid format {} in {:x}", bufFormat, (uintptr_t)this);
+ return ERROR_BUFFER_FORMAT;
+ }
+
+ m_pendingFrame.pending = true;
+ m_pendingFrame.monitor = monitor;
+ m_pendingFrame.buffer = buffer;
+ m_pendingFrame.sourceBoxCallback = sourceBoxCallback;
+ m_pendingFrame.callback = callback;
+
+ // nothing changed, then delay copy until contraints changed
+ if (!m_constraintsChanged)
+ return ERROR_NONE;
+
+ if (!copy()) {
+ LOGM(Log::ERR, "Failed to copy cursor image for cursor share");
+ callback(RESULT_NOT_COPIED);
+ m_pendingFrame.pending = false;
+ return ERROR_UNKNOWN;
+ }
+
+ return ERROR_NONE;
+}
+
+void CCursorshareSession::render() {
+ const auto PERM = g_pDynamicPermissionManager->clientPermissionMode(m_client, PERMISSION_TYPE_CURSOR_POS);
+
+ const auto& cursorImage = g_pPointerManager->currentCursorImage();
+
+ // TODO: implement a monitor independent render mode to buffer that does this in CHyprRenderer::begin() or something like that
+ g_pHyprOpenGL->m_renderData.transformDamage = false;
+ g_pHyprOpenGL->setViewport(0, 0, m_bufferSize.x, m_bufferSize.y);
+
+ bool overlaps = g_pPointerManager->getCursorBoxGlobal().overlaps(m_pendingFrame.sourceBoxCallback());
+ if (PERM != PERMISSION_RULE_ALLOW_MODE_ALLOW || !overlaps) {
+ // render black when not allowed
+ g_pHyprOpenGL->clear(Colors::BLACK);
+ } else if (!cursorImage.pBuffer || !cursorImage.surface || !cursorImage.bufferTex) {
+ // render clear when cursor is probably hidden
+ g_pHyprOpenGL->clear(CHyprColor(0, 0, 0, 0));
+ } else {
+ // render cursor
+ CBox texbox = {{}, cursorImage.bufferTex->m_size};
+ g_pHyprOpenGL->renderTexture(cursorImage.bufferTex, texbox, {});
+ }
+
+ g_pHyprOpenGL->m_renderData.blockScreenShader = true;
+}
+
+bool CCursorshareSession::copy() {
+ if (!m_pendingFrame.callback || !m_pendingFrame.monitor || !m_pendingFrame.callback || !m_pendingFrame.sourceBoxCallback)
+ return false;
+
+ // FIXME: this doesn't really make sense but just to be safe
+ m_pendingFrame.callback(RESULT_TIMESTAMP);
+
+ g_pHyprRenderer->makeEGLCurrent();
+
+ CRegion fakeDamage = {0, 0, INT16_MAX, INT16_MAX};
+ if (auto attrs = m_pendingFrame.buffer->dmabuf(); attrs.success) {
+ if (attrs.format != m_format) {
+ LOGM(Log::ERR, "Can't copy: invalid format");
+ return false;
+ }
+
+ if (!g_pHyprRenderer->beginRender(m_pendingFrame.monitor, fakeDamage, RENDER_MODE_TO_BUFFER, m_pendingFrame.buffer, nullptr, true)) {
+ LOGM(Log::ERR, "Can't copy: failed to begin rendering to dmabuf");
+ return false;
+ }
+
+ render();
+
+ g_pHyprRenderer->endRender([callback = m_pendingFrame.callback]() {
+ if (callback)
+ callback(RESULT_COPIED);
+ });
+ } else if (auto attrs = m_pendingFrame.buffer->shm(); attrs.success) {
+ auto [bufData, fmt, bufLen] = m_pendingFrame.buffer->beginDataPtr(0);
+ const auto PFORMAT = NFormatUtils::getPixelFormatFromDRM(m_format);
+
+ if (attrs.format != m_format || !PFORMAT) {
+ LOGM(Log::ERR, "Can't copy: invalid format");
+ return false;
+ }
+
+ CFramebuffer outFB;
+ outFB.alloc(m_bufferSize.x, m_bufferSize.y, m_format);
+
+ if (!g_pHyprRenderer->beginRender(m_pendingFrame.monitor, fakeDamage, RENDER_MODE_FULL_FAKE, nullptr, &outFB, true)) {
+ LOGM(Log::ERR, "Can't copy: failed to begin rendering to shm");
+ return false;
+ }
+
+ render();
+
+ g_pHyprRenderer->endRender();
+
+ g_pHyprOpenGL->m_renderData.pMonitor = m_pendingFrame.monitor;
+ outFB.bind();
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, outFB.getFBID());
+
+ glPixelStorei(GL_PACK_ALIGNMENT, 1);
+
+ int glFormat = PFORMAT->glFormat;
+
+ if (glFormat == GL_RGBA)
+ glFormat = GL_BGRA_EXT;
+
+ if (glFormat != GL_BGRA_EXT && glFormat != GL_RGB) {
+ if (PFORMAT->swizzle.has_value()) {
+ std::array<GLint, 4> RGBA = SWIZZLE_RGBA;
+ std::array<GLint, 4> BGRA = SWIZZLE_BGRA;
+ if (PFORMAT->swizzle == RGBA)
+ glFormat = GL_RGBA;
+ else if (PFORMAT->swizzle == BGRA)
+ glFormat = GL_BGRA_EXT;
+ else {
+ LOGM(Log::ERR, "Copied frame via shm might be broken or color flipped");
+ glFormat = GL_RGBA;
+ }
+ }
+ }
+
+ glReadPixels(0, 0, m_bufferSize.x, m_bufferSize.y, glFormat, PFORMAT->glType, bufData);
+
+ g_pHyprOpenGL->m_renderData.pMonitor.reset();
+
+ m_pendingFrame.buffer->endDataPtr();
+ outFB.unbind();
+ glPixelStorei(GL_PACK_ALIGNMENT, 4);
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
+
+ m_pendingFrame.callback(RESULT_COPIED);
+ } else {
+ LOGM(Log::ERR, "Can't copy: invalid buffer type");
+ return false;
+ }
+
+ m_pendingFrame.pending = false;
+ m_constraintsChanged = false;
+ return true;
+}
+
+DRMFormat CCursorshareSession::format() const {
+ return m_format;
+}
+
+Vector2D CCursorshareSession::bufferSize() const {
+ return m_bufferSize;
+}
+
+Vector2D CCursorshareSession::hotspot() const {
+ return m_hotspot;
+}
diff --git a/src/managers/screenshare/ScreenshareFrame.cpp b/src/managers/screenshare/ScreenshareFrame.cpp
new file mode 100644
index 00000000..73ccf958
--- /dev/null
+++ b/src/managers/screenshare/ScreenshareFrame.cpp
@@ -0,0 +1,493 @@
+#include "ScreenshareManager.hpp"
+#include "../PointerManager.hpp"
+#include "../input/InputManager.hpp"
+#include "../permissions/DynamicPermissionManager.hpp"
+#include "../../protocols/ColorManagement.hpp"
+#include "../../protocols/XDGShell.hpp"
+#include "../../Compositor.hpp"
+#include "../../render/Renderer.hpp"
+#include "../../render/OpenGL.hpp"
+#include "../../helpers/Monitor.hpp"
+#include "../../desktop/view/Window.hpp"
+#include "../../desktop/state/FocusState.hpp"
+
+using namespace Screenshare;
+
+CScreenshareFrame::CScreenshareFrame(WP<CScreenshareSession> session, bool overlayCursor, bool isFirst) :
+ m_session(session), m_bufferSize(m_session->bufferSize()), m_overlayCursor(overlayCursor), m_isFirst(isFirst) {
+ ;
+}
+
+CScreenshareFrame::~CScreenshareFrame() {
+ if (m_failed || !m_shared)
+ return;
+
+ if (!m_copied && m_callback)
+ m_callback(RESULT_NOT_COPIED);
+}
+
+bool CScreenshareFrame::done() const {
+ if (m_session.expired() || m_session->m_stopped)
+ return true;
+
+ if (m_session->m_type == SHARE_NONE || m_bufferSize == Vector2D(0, 0))
+ return true;
+
+ if (m_failed || m_copied)
+ return true;
+
+ if (m_session->m_type == SHARE_MONITOR && !m_session->monitor())
+ return true;
+
+ if (m_session->m_type == SHARE_REGION && !m_session->monitor())
+ return true;
+
+ if (m_session->m_type == SHARE_WINDOW && (!m_session->monitor() || !validMapped(m_session->m_window)))
+ return true;
+
+ if (!m_shared)
+ return false;
+
+ if (!m_buffer || !m_buffer->m_resource || !m_buffer->m_resource->good())
+ return true;
+
+ if (!m_callback)
+ return true;
+
+ return false;
+}
+
+eScreenshareError CScreenshareFrame::share(SP<IHLBuffer> buffer, const CRegion& clientDamage, FScreenshareCallback callback) {
+ if UNLIKELY (done())
+ return ERROR_STOPPED;
+
+ if UNLIKELY (!m_session->monitor() || !g_pCompositor->monitorExists(m_session->monitor())) {
+ LOGM(Log::ERR, "Client requested sharing of a monitor that is gone");
+ m_failed = true;
+ return ERROR_STOPPED;
+ }
+
+ if UNLIKELY (m_session->m_type == SHARE_WINDOW && !validMapped(m_session->m_window)) {
+ LOGM(Log::ERR, "Client requested sharing of window that is gone or not shareable!");
+ m_failed = true;
+ return ERROR_STOPPED;
+ }
+
+ if UNLIKELY (!buffer || !buffer->m_resource || !buffer->m_resource->good()) {
+ LOGM(Log::ERR, "Client requested sharing to an invalid buffer");
+ return ERROR_NO_BUFFER;
+ }
+
+ if UNLIKELY (buffer->size != m_bufferSize) {
+ LOGM(Log::ERR, "Client requested sharing to an invalid buffer size");
+ return ERROR_BUFFER_SIZE;
+ }
+
+ uint32_t bufFormat;
+ if (buffer->dmabuf().success)
+ bufFormat = buffer->dmabuf().format;
+ else if (buffer->shm().success)
+ bufFormat = buffer->shm().format;
+ else {
+ LOGM(Log::ERR, "Client requested sharing to an invalid buffer");
+ return ERROR_NO_BUFFER;
+ }
+
+ if (std::ranges::count_if(m_session->allowedFormats(), [&](const DRMFormat& format) { return format == bufFormat; }) == 0) {
+ LOGM(Log::ERR, "Invalid format {} in {:x}", bufFormat, (uintptr_t)this);
+ return ERROR_BUFFER_FORMAT;
+ }
+
+ m_buffer = buffer;
+ m_callback = callback;
+ m_shared = true;
+
+ // schedule a frame so that when a screenshare starts it isn't black until the output is updated
+ if (m_isFirst) {
+ g_pCompositor->scheduleFrameForMonitor(m_session->monitor(), Aquamarine::IOutput::AQ_SCHEDULE_NEEDS_FRAME);
+ g_pHyprRenderer->damageMonitor(m_session->monitor());
+ }
+
+ // TODO: add a damage ring for output damage since last shared frame
+ CRegion frameDamage = CRegion(0, 0, m_bufferSize.x, m_bufferSize.y);
+
+ // copy everything on the first frame
+ if (m_isFirst)
+ m_damage = CRegion(0, 0, m_bufferSize.x, m_bufferSize.y);
+ else
+ m_damage = frameDamage.add(clientDamage);
+
+ m_damage.intersect(0, 0, m_bufferSize.x, m_bufferSize.y);
+
+ return ERROR_NONE;
+}
+
+void CScreenshareFrame::copy() {
+ if (done())
+ return;
+
+ // tell client to send presented timestamp
+ // TODO: is this right? this is right after we commit to aq, not when page flip happens..
+ m_callback(RESULT_TIMESTAMP);
+
+ // store a snapshot before the permission popup so we don't break screenshots
+ const auto PERM = g_pDynamicPermissionManager->clientPermissionMode(m_session->m_client, PERMISSION_TYPE_SCREENCOPY);
+ if (PERM == PERMISSION_RULE_ALLOW_MODE_PENDING) {
+ if (!m_session->m_tempFB.isAllocated())
+ storeTempFB();
+
+ // don't copy a frame while allow is pending because screenshot tools will only take the first frame we give, which is empty
+ return;
+ }
+
+ if (m_buffer->shm().success)
+ m_failed = !copyShm();
+ else if (m_buffer->dmabuf().success)
+ m_failed = !copyDmabuf();
+
+ if (!m_failed) {
+ // screensharing has started again
+ m_session->screenshareEvents(true);
+ m_session->m_shareStopTimer->updateTimeout(std::chrono::milliseconds(500)); // check in half second
+ } else
+ m_callback(RESULT_NOT_COPIED);
+}
+
+void CScreenshareFrame::renderMonitor() {
+ if ((m_session->m_type != SHARE_MONITOR && m_session->m_type != SHARE_REGION) || done())
+ return;
+
+ const auto PMONITOR = m_session->monitor();
+
+ auto TEXTURE = makeShared<CTexture>(PMONITOR->m_output->state->state().buffer);
+
+ const bool IS_CM_AWARE = PROTO::colorManagement && PROTO::colorManagement->isClientCMAware(m_session->m_client);
+ g_pHyprOpenGL->m_renderData.transformDamage = false;
+ g_pHyprOpenGL->m_renderData.noSimplify = true;
+
+ // render monitor texture
+ CBox monbox = CBox{{}, PMONITOR->m_pixelSize}
+ .transform(Math::wlTransformToHyprutils(Math::invertTransform(PMONITOR->m_transform)), PMONITOR->m_pixelSize.x, PMONITOR->m_pixelSize.y)
+ .translate(-m_session->m_captureBox.pos()); // vvvv kinda ass-backwards but that's how I designed the renderer... sigh.
+ g_pHyprOpenGL->pushMonitorTransformEnabled(true);
+ g_pHyprOpenGL->setRenderModifEnabled(false);
+ g_pHyprOpenGL->renderTexture(TEXTURE, monbox,
+ {
+ .cmBackToSRGB = !IS_CM_AWARE,
+ .cmBackToSRGBSource = !IS_CM_AWARE ? PMONITOR : nullptr,
+ });
+ g_pHyprOpenGL->setRenderModifEnabled(true);
+ g_pHyprOpenGL->popMonitorTransformEnabled();
+
+ // render black boxes for noscreenshare
+ auto hidePopups = [&](Vector2D popupBaseOffset) {
+ return [&, popupBaseOffset](WP<Desktop::View::CPopup> popup, void*) {
+ if (!popup->wlSurface() || !popup->wlSurface()->resource() || !popup->visible())
+ return;
+
+ const auto popRel = popup->coordsRelativeToParent();
+ popup->wlSurface()->resource()->breadthfirst(
+ [&](SP<CWLSurfaceResource> surf, const Vector2D& localOff, void*) {
+ const auto size = surf->m_current.size;
+ const auto surfBox =
+ CBox{popupBaseOffset + popRel + localOff, size}.translate(PMONITOR->m_position).scale(PMONITOR->m_scale).translate(-m_session->m_captureBox.pos());
+
+ if LIKELY (surfBox.w > 0 && surfBox.h > 0)
+ g_pHyprOpenGL->renderRect(surfBox, Colors::BLACK, {});
+ },
+ nullptr);
+ };
+ };
+
+ for (auto const& l : g_pCompositor->m_layers) {
+ if (!l->m_ruleApplicator->noScreenShare().valueOrDefault())
+ continue;
+
+ if UNLIKELY (!l->visible())
+ continue;
+
+ const auto REALPOS = l->m_realPosition->value();
+ const auto REALSIZE = l->m_realSize->value();
+
+ const auto noScreenShareBox = CBox{REALPOS.x, REALPOS.y, std::max(REALSIZE.x, 5.0), std::max(REALSIZE.y, 5.0)}
+ .translate(-PMONITOR->m_position)
+ .scale(PMONITOR->m_scale)
+ .translate(-m_session->m_captureBox.pos());
+
+ g_pHyprOpenGL->renderRect(noScreenShareBox, Colors::BLACK, {});
+
+ const auto geom = l->m_geometry;
+ const Vector2D popupBaseOffset = REALPOS - Vector2D{geom.pos().x, geom.pos().y};
+ if (l->m_popupHead)
+ l->m_popupHead->breadthfirst(hidePopups(popupBaseOffset), nullptr);
+ }
+
+ for (auto const& w : g_pCompositor->m_windows) {
+ if (!w->m_ruleApplicator->noScreenShare().valueOrDefault())
+ continue;
+
+ if (!g_pHyprRenderer->shouldRenderWindow(w, PMONITOR))
+ continue;
+
+ if (w->isHidden())
+ continue;
+
+ const auto PWORKSPACE = w->m_workspace;
+
+ if UNLIKELY (!PWORKSPACE && !w->m_fadingOut && w->m_alpha->value() != 0.f)
+ continue;
+
+ const auto renderOffset = PWORKSPACE && !w->m_pinned ? PWORKSPACE->m_renderOffset->value() : Vector2D{};
+ const auto REALPOS = w->m_realPosition->value() + renderOffset;
+ const auto noScreenShareBox = CBox{REALPOS.x, REALPOS.y, std::max(w->m_realSize->value().x, 5.0), std::max(w->m_realSize->value().y, 5.0)}
+ .translate(-PMONITOR->m_position)
+ .scale(PMONITOR->m_scale)
+ .translate(-m_session->m_captureBox.pos());
+
+ // seems like rounding doesn't play well with how we manipulate the box position to render regions causing the window to leak through
+ const auto dontRound = m_session->m_captureBox.pos() != Vector2D() || w->isEffectiveInternalFSMode(FSMODE_FULLSCREEN);
+ const auto rounding = dontRound ? 0 : w->rounding() * PMONITOR->m_scale;
+ const auto roundingPower = dontRound ? 2.0f : w->roundingPower();
+
+ g_pHyprOpenGL->renderRect(noScreenShareBox, Colors::BLACK, {.round = rounding, .roundingPower = roundingPower});
+
+ if (w->m_isX11 || !w->m_popupHead)
+ continue;
+
+ const auto geom = w->m_xdgSurface->m_current.geometry;
+ const Vector2D popupBaseOffset = REALPOS - Vector2D{geom.pos().x, geom.pos().y};
+
+ w->m_popupHead->breadthfirst(hidePopups(popupBaseOffset), nullptr);
+ }
+
+ if (m_overlayCursor) {
+ CRegion fakeDamage = {0, 0, INT16_MAX, INT16_MAX};
+ Vector2D cursorPos = g_pInputManager->getMouseCoordsInternal() - PMONITOR->m_position - m_session->m_captureBox.pos() / PMONITOR->m_scale;
+ g_pPointerManager->renderSoftwareCursorsFor(PMONITOR, Time::steadyNow(), fakeDamage, cursorPos, true);
+ }
+}
+
+void CScreenshareFrame::renderWindow() {
+ if (m_session->m_type != SHARE_WINDOW || done())
+ return;
+
+ const auto PWINDOW = m_session->m_window.lock();
+ const auto PMONITOR = m_session->monitor();
+
+ const auto NOW = Time::steadyNow();
+
+ // TODO: implement a monitor independent render mode to buffer that does this in CHyprRenderer::begin() or something like that
+ g_pHyprOpenGL->m_renderData.monitorProjection = Mat3x3::identity();
+ g_pHyprOpenGL->m_renderData.projection = Mat3x3::outputProjection(m_bufferSize, HYPRUTILS_TRANSFORM_NORMAL);
+ g_pHyprOpenGL->m_renderData.transformDamage = false;
+ g_pHyprOpenGL->setViewport(0, 0, m_bufferSize.x, m_bufferSize.y);
+
+ g_pHyprRenderer->m_bBlockSurfaceFeedback = g_pHyprRenderer->shouldRenderWindow(PWINDOW); // block the feedback to avoid spamming the surface if it's visible
+ g_pHyprRenderer->renderWindow(PWINDOW, PMONITOR, NOW, false, RENDER_PASS_ALL, true, true);
+ g_pHyprRenderer->m_bBlockSurfaceFeedback = false;
+
+ if (!m_overlayCursor)
+ return;
+
+ auto pointerSurfaceResource = g_pSeatManager->m_state.pointerFocus.lock();
+
+ if (!pointerSurfaceResource)
+ return;
+
+ auto pointerSurface = Desktop::View::CWLSurface::fromResource(pointerSurfaceResource);
+
+ if (!pointerSurface || pointerSurface->getSurfaceBoxGlobal()->intersection(m_session->m_window->getFullWindowBoundingBox()).empty())
+ return;
+
+ if (Desktop::focusState()->window() != m_session->m_window)
+ return;
+
+ CRegion fakeDamage = {0, 0, INT16_MAX, INT16_MAX};
+ g_pPointerManager->renderSoftwareCursorsFor(PMONITOR->m_self.lock(), NOW, fakeDamage, g_pInputManager->getMouseCoordsInternal() - PWINDOW->m_realPosition->value(), true);
+}
+
+void CScreenshareFrame::render() {
+ const auto PERM = g_pDynamicPermissionManager->clientPermissionMode(m_session->m_client, PERMISSION_TYPE_SCREENCOPY);
+
+ if (PERM == PERMISSION_RULE_ALLOW_MODE_PENDING) {
+ g_pHyprOpenGL->clear(CHyprColor(0, 0, 0, 0));
+ return;
+ }
+
+ bool windowShareDenied = m_session->m_type == SHARE_WINDOW && m_session->m_window->m_ruleApplicator && m_session->m_window->m_ruleApplicator->noScreenShare().valueOrDefault();
+ if (PERM == PERMISSION_RULE_ALLOW_MODE_DENY || windowShareDenied) {
+ g_pHyprOpenGL->clear(CHyprColor(0, 0, 0, 0));
+ CBox texbox = CBox{m_bufferSize / 2.F, g_pHyprOpenGL->m_screencopyDeniedTexture->m_size}.translate(-g_pHyprOpenGL->m_screencopyDeniedTexture->m_size / 2.F);
+ g_pHyprOpenGL->renderTexture(g_pHyprOpenGL->m_screencopyDeniedTexture, texbox, {});
+ return;
+ }
+
+ if (m_session->m_tempFB.isAllocated()) {
+ CBox texbox = {{}, m_bufferSize};
+ g_pHyprOpenGL->renderTexture(m_session->m_tempFB.getTexture(), texbox, {});
+ m_session->m_tempFB.release();
+ return;
+ }
+
+ switch (m_session->m_type) {
+ case SHARE_REGION: // TODO: could this be better? this is how screencopy works
+ case SHARE_MONITOR: renderMonitor(); break;
+ case SHARE_WINDOW: renderWindow(); break;
+ case SHARE_NONE:
+ default: return;
+ }
+}
+
+bool CScreenshareFrame::copyDmabuf() {
+ if (done())
+ return false;
+
+ if (!g_pHyprRenderer->beginRender(m_session->monitor(), m_damage, RENDER_MODE_TO_BUFFER, m_buffer, nullptr, true)) {
+ LOGM(Log::ERR, "Can't copy: failed to begin rendering to dma frame");
+ return false;
+ }
+
+ render();
+
+ g_pHyprOpenGL->m_renderData.blockScreenShader = true;
+
+ g_pHyprRenderer->endRender([self = m_self]() {
+ if (!self || self.expired() || self->m_copied)
+ return;
+
+ LOGM(Log::TRACE, "Copied frame via dma");
+ self->m_callback(RESULT_COPIED);
+ self->m_copied = true;
+ });
+
+ return true;
+}
+
+bool CScreenshareFrame::copyShm() {
+ if (done())
+ return false;
+
+ g_pHyprRenderer->makeEGLCurrent();
+
+ auto shm = m_buffer->shm();
+ auto [pixelData, fmt, bufLen] = m_buffer->beginDataPtr(0); // no need for end, cuz it's shm
+
+ const auto PFORMAT = NFormatUtils::getPixelFormatFromDRM(shm.format);
+ if (!PFORMAT) {
+ LOGM(Log::ERR, "Can't copy: failed to find a pixel format");
+ return false;
+ }
+
+ const auto PMONITOR = m_session->monitor();
+
+ CFramebuffer outFB;
+ outFB.alloc(m_bufferSize.x, m_bufferSize.y, shm.format);
+
+ if (!g_pHyprRenderer->beginRender(PMONITOR, m_damage, RENDER_MODE_FULL_FAKE, nullptr, &outFB, true)) {
+ LOGM(Log::ERR, "Can't copy: failed to begin rendering");
+ return false;
+ }
+
+ render();
+
+ g_pHyprOpenGL->m_renderData.blockScreenShader = true;
+
+ g_pHyprRenderer->endRender();
+
+ g_pHyprOpenGL->m_renderData.pMonitor = PMONITOR;
+ outFB.bind();
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, outFB.getFBID());
+
+ glPixelStorei(GL_PACK_ALIGNMENT, 1);
+
+ uint32_t packStride = NFormatUtils::minStride(PFORMAT, m_bufferSize.x);
+ int glFormat = PFORMAT->glFormat;
+
+ if (glFormat == GL_RGBA)
+ glFormat = GL_BGRA_EXT;
+
+ if (glFormat != GL_BGRA_EXT && glFormat != GL_RGB) {
+ if (PFORMAT->swizzle.has_value()) {
+ std::array<GLint, 4> RGBA = SWIZZLE_RGBA;
+ std::array<GLint, 4> BGRA = SWIZZLE_BGRA;
+ if (PFORMAT->swizzle == RGBA)
+ glFormat = GL_RGBA;
+ else if (PFORMAT->swizzle == BGRA)
+ glFormat = GL_BGRA_EXT;
+ else {
+ LOGM(Log::ERR, "Copied frame via shm might be broken or color flipped");
+ glFormat = GL_RGBA;
+ }
+ }
+ }
+
+ // TODO: use pixel buffer object to not block cpu
+ if (packStride == sc<uint32_t>(shm.stride)) {
+ m_damage.forEachRect([&](const auto& rect) {
+ int width = rect.x2 - rect.x1;
+ int height = rect.y2 - rect.y1;
+ glReadPixels(rect.x1, rect.y1, width, height, glFormat, PFORMAT->glType, pixelData);
+ });
+ } else {
+ m_damage.forEachRect([&](const auto& rect) {
+ size_t width = rect.x2 - rect.x1;
+ size_t height = rect.y2 - rect.y1;
+ for (size_t i = rect.y1; i < height; ++i) {
+ glReadPixels(rect.x1, i, width, 1, glFormat, PFORMAT->glType, pixelData + (rect.x1 * PFORMAT->bytesPerBlock) + (i * shm.stride));
+ }
+ });
+ }
+
+ outFB.unbind();
+ glPixelStorei(GL_PACK_ALIGNMENT, 4);
+ glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
+
+ g_pHyprOpenGL->m_renderData.pMonitor.reset();
+
+ if (!m_copied) {
+ LOGM(Log::TRACE, "Copied frame via shm");
+ m_callback(RESULT_COPIED);
+ }
+
+ return true;
+}
+
+void CScreenshareFrame::storeTempFB() {
+ g_pHyprRenderer->makeEGLCurrent();
+
+ m_session->m_tempFB.alloc(m_bufferSize.x, m_bufferSize.y);
+
+ CRegion fakeDamage = {0, 0, INT16_MAX, INT16_MAX};
+
+ if (!g_pHyprRenderer->beginRender(m_session->monitor(), fakeDamage, RENDER_MODE_FULL_FAKE, nullptr, &m_session->m_tempFB, true)) {
+ LOGM(Log::ERR, "Can't copy: failed to begin rendering to temp fb");
+ return;
+ }
+
+ switch (m_session->m_type) {
+ case SHARE_REGION: // TODO: could this be better? this is how screencopy works
+ case SHARE_MONITOR: renderMonitor(); break;
+ case SHARE_WINDOW: renderWindow(); break;
+ case SHARE_NONE:
+ default: return;
+ }
+
+ g_pHyprRenderer->endRender();
+}
+
+Vector2D CScreenshareFrame::bufferSize() const {
+ return m_bufferSize;
+}
+
+wl_output_transform CScreenshareFrame::transform() const {
+ switch (m_session->m_type) {
+ case SHARE_REGION:
+ case SHARE_MONITOR: return m_session->monitor()->m_transform;
+ default:
+ case SHARE_WINDOW: return WL_OUTPUT_TRANSFORM_NORMAL;
+ }
+}
+
+const CRegion& CScreenshareFrame::damage() const {
+ return m_damage;
+}
diff --git a/src/managers/screenshare/ScreenshareManager.cpp b/src/managers/screenshare/ScreenshareManager.cpp
new file mode 100644
index 00000000..823e99b3
--- /dev/null
+++ b/src/managers/screenshare/ScreenshareManager.cpp
@@ -0,0 +1,161 @@
+#include "ScreenshareManager.hpp"
+#include "../../render/Renderer.hpp"
+#include "../../Compositor.hpp"
+#include "../../desktop/view/Window.hpp"
+#include "../../protocols/core/Seat.hpp"
+
+using namespace Screenshare;
+
+CScreenshareManager::CScreenshareManager() {
+ ;
+}
+
+void CScreenshareManager::onOutputCommit(PHLMONITOR monitor) {
+ std::erase_if(m_sessions, [&](const WP<CScreenshareSession>& session) { return session.expired(); });
+
+ // if no pending frames, and no sessions are sharing, then unblock ds
+ if (m_pendingFrames.empty()) {
+ for (const auto& session : m_sessions) {
+ if (!session->m_stopped && session->m_sharing)
+ return;
+ }
+
+ g_pHyprRenderer->m_directScanoutBlocked = false;
+ return; // nothing to share
+ }
+
+ std::ranges::for_each(m_pendingFrames, [&](WP<CScreenshareFrame>& frame) {
+ if (frame.expired() || !frame->m_shared || frame->done())
+ return;
+
+ if (frame->m_session->monitor() != monitor)
+ return;
+
+ if (frame->m_session->m_type == SHARE_WINDOW) {
+ CBox geometry = {frame->m_session->m_window->m_realPosition->value(), frame->m_session->m_window->m_realSize->value()};
+ if (geometry.intersection({monitor->m_position, monitor->m_size}).empty())
+ return;
+ }
+
+ frame->copy();
+ });
+
+ std::erase_if(m_pendingFrames, [&](const WP<CScreenshareFrame>& frame) { return frame.expired(); });
+}
+
+UP<CScreenshareSession> CScreenshareManager::newSession(wl_client* client, PHLMONITOR monitor) {
+ if UNLIKELY (!monitor || !g_pCompositor->monitorExists(monitor)) {
+ LOGM(Log::ERR, "Client requested sharing of a monitor that is gone");
+ return nullptr;
+ }
+
+ UP<CScreenshareSession> session = UP<CScreenshareSession>(new CScreenshareSession(monitor, client));
+
+ session->m_self = session;
+ m_sessions.emplace_back(session);
+
+ return session;
+}
+
+UP<CScreenshareSession> CScreenshareManager::newSession(wl_client* client, PHLMONITOR monitor, CBox captureRegion) {
+ if UNLIKELY (!monitor || !g_pCompositor->monitorExists(monitor)) {
+ LOGM(Log::ERR, "Client requested sharing of a monitor that is gone");
+ return nullptr;
+ }
+
+ UP<CScreenshareSession> session = UP<CScreenshareSession>(new CScreenshareSession(monitor, captureRegion, client));
+
+ session->m_self = session;
+ m_sessions.emplace_back(session);
+
+ return session;
+}
+
+UP<CScreenshareSession> CScreenshareManager::newSession(wl_client* client, PHLWINDOW window) {
+ if UNLIKELY (!window || !window->m_isMapped) {
+ LOGM(Log::ERR, "Client requested sharing of window that is gone or not shareable!");
+ return nullptr;
+ }
+
+ UP<CScreenshareSession> session = UP<CScreenshareSession>(new CScreenshareSession(window, client));
+
+ session->m_self = session;
+ m_sessions.emplace_back(session);
+
+ return session;
+}
+
+UP<CCursorshareSession> CScreenshareManager::newCursorSession(wl_client* client, WP<CWLPointerResource> pointer) {
+ UP<CCursorshareSession> session = UP<CCursorshareSession>(new CCursorshareSession(client, pointer));
+
+ session->m_self = session;
+ m_cursorSessions.emplace_back(session);
+
+ return session;
+}
+
+WP<CScreenshareSession> CScreenshareManager::getManagedSession(wl_client* client, PHLMONITOR monitor) {
+ return getManagedSession(SHARE_MONITOR, client, monitor, nullptr, {});
+}
+
+WP<CScreenshareSession> CScreenshareManager::getManagedSession(wl_client* client, PHLMONITOR monitor, CBox captureBox) {
+
+ return getManagedSession(SHARE_REGION, client, monitor, nullptr, captureBox);
+}
+
+WP<CScreenshareSession> CScreenshareManager::getManagedSession(wl_client* client, PHLWINDOW window) {
+ return getManagedSession(SHARE_WINDOW, client, nullptr, window, {});
+}
+
+WP<CScreenshareSession> CScreenshareManager::getManagedSession(eScreenshareType type, wl_client* client, PHLMONITOR monitor, PHLWINDOW window, CBox captureBox) {
+ if (type == SHARE_NONE)
+ return {};
+
+ auto it = std::ranges::find_if(m_managedSessions, [&](const auto& session) {
+ if (session->m_session->m_client != client || session->m_session->m_type != type)
+ return false;
+
+ switch (type) {
+ case SHARE_MONITOR: return session->m_session->m_monitor == monitor;
+ case SHARE_WINDOW: return session->m_session->m_window == window;
+ case SHARE_REGION: return session->m_session->m_monitor == monitor && session->m_session->m_captureBox == captureBox;
+ case SHARE_NONE:
+ default: return false;
+ }
+
+ return false;
+ });
+
+ if (it == m_managedSessions.end()) {
+ UP<CScreenshareSession> session;
+ switch (type) {
+ case SHARE_MONITOR: session = UP<CScreenshareSession>(new CScreenshareSession(monitor, client)); break;
+ case SHARE_WINDOW: session = UP<CScreenshareSession>(new CScreenshareSession(window, client)); break;
+ case SHARE_REGION: session = UP<CScreenshareSession>(new CScreenshareSession(monitor, captureBox, client)); break;
+ case SHARE_NONE:
+ default: return {};
+ }
+
+ session->m_self = session;
+ m_sessions.emplace_back(session);
+
+ it = m_managedSessions.emplace(m_managedSessions.end(), makeUnique<SManagedSession>(std::move(session)));
+ }
+
+ auto& session = *it;
+
+ session->stoppedListener = session->m_session->m_events.stopped.listen([session = WP<SManagedSession>(session)]() {
+ std::erase_if(Screenshare::mgr()->m_managedSessions, [&](const auto& s) { return !s || session.expired() || s->m_session == session->m_session; });
+ });
+
+ return session->m_session;
+}
+
+void CScreenshareManager::destroyClientSessions(wl_client* client) {
+ LOGM(Log::TRACE, "Destroy client sessions for {:x}", (uintptr_t)client);
+ std::erase_if(m_managedSessions, [&](const auto& session) { return !session || session->m_session->m_client == client; });
+}
+
+CScreenshareManager::SManagedSession::SManagedSession(UP<CScreenshareSession>&& session) : m_session(std::move(session)) {
+ ;
+}
diff --git a/src/managers/screenshare/ScreenshareManager.hpp b/src/managers/screenshare/ScreenshareManager.hpp
new file mode 100644
index 00000000..4c61a7b0
--- /dev/null
+++ b/src/managers/screenshare/ScreenshareManager.hpp
@@ -0,0 +1,252 @@
+#pragma once
+
+#include <vector>
+#include "../../helpers/memory/Memory.hpp"
+#include "../../protocols/types/Buffer.hpp"
+#include "../../render/Framebuffer.hpp"
+#include "../eventLoop/EventLoopTimer.hpp"
+#include "../../render/Renderer.hpp"
+
+// TODO: do screenshare damage
+
+class CWLPointerResource;
+
+namespace Screenshare {
+ enum eScreenshareType : uint8_t {
+ SHARE_MONITOR,
+ SHARE_WINDOW,
+ SHARE_REGION,
+ SHARE_NONE
+ };
+
+ enum eScreenshareError : uint8_t {
+ ERROR_NONE,
+ ERROR_UNKNOWN,
+ ERROR_STOPPED,
+ ERROR_NO_BUFFER,
+ ERROR_BUFFER_SIZE,
+ ERROR_BUFFER_FORMAT
+ };
+
+ enum eScreenshareResult : uint8_t {
+ RESULT_COPIED,
+ RESULT_NOT_COPIED,
+ RESULT_TIMESTAMP,
+ };
+
+ using FScreenshareCallback = std::function<void(eScreenshareResult result)>;
+ using FSourceBoxCallback = std::function<CBox(void)>;
+
+ class CScreenshareSession {
+ public:
+ CScreenshareSession(const CScreenshareSession&) = delete;
+ CScreenshareSession(CScreenshareSession&&) = delete;
+ ~CScreenshareSession();
+
+ UP<CScreenshareFrame> nextFrame(bool overlayCursor);
+ void stop();
+
+ // constraints
+ const std::vector<DRMFormat>& allowedFormats() const;
+ Vector2D bufferSize() const;
+ PHLMONITOR monitor() const; // this will return the correct monitor based on type
+
+ struct {
+ CSignalT<> stopped;
+ CSignalT<> constraintsChanged;
+ } m_events;
+
+ private:
+ CScreenshareSession(PHLMONITOR monitor, wl_client* client);
+ CScreenshareSession(PHLMONITOR monitor, CBox captureRegion, wl_client* client);
+ CScreenshareSession(PHLWINDOW window, wl_client* client);
+
+ WP<CScreenshareSession> m_self;
+ bool m_stopped = false;
+
+ eScreenshareType m_type = SHARE_NONE;
+ PHLMONITORREF m_monitor;
+ PHLWINDOWREF m_window;
+ CBox m_captureBox = {}; // given capture area in logical coordinates (see xdg_output)
+
+ wl_client* m_client = nullptr;
+ std::string m_name = "";
+
+ std::vector<DRMFormat> m_formats;
+ Vector2D m_bufferSize = Vector2D(0, 0);
+
+ CFramebuffer m_tempFB;
+
+ SP<CEventLoopTimer> m_shareStopTimer;
+ bool m_sharing = false;
+
+ struct {
+ CHyprSignalListener monitorDestroyed;
+ CHyprSignalListener monitorModeChanged;
+ CHyprSignalListener windowDestroyed;
+ CHyprSignalListener windowSizeChanged;
+ CHyprSignalListener windowMonitorChanged;
+ } m_listeners;
+
+ void screenshareEvents(bool started);
+ void calculateConstraints();
+ void init();
+
+ friend class CScreenshareFrame;
+ friend class CScreenshareManager;
+ };
+
+ class CCursorshareSession {
+ public:
+ CCursorshareSession(const CCursorshareSession&) = delete;
+ CCursorshareSession(CCursorshareSession&&) = delete;
+ ~CCursorshareSession();
+
+ eScreenshareError share(PHLMONITOR monitor, SP<IHLBuffer> buffer, FSourceBoxCallback sourceBoxCallback, FScreenshareCallback callback);
+ void stop();
+
+ // constraints
+ DRMFormat format() const;
+ Vector2D bufferSize() const;
+ Vector2D hotspot() const;
+
+ struct {
+ CSignalT<> stopped;
+ CSignalT<> constraintsChanged;
+ } m_events;
+
+ private:
+ CCursorshareSession(wl_client* client, WP<CWLPointerResource> pointer);
+
+ WP<CCursorshareSession> m_self;
+ bool m_stopped = false;
+ bool m_constraintsChanged = true;
+
+ wl_client* m_client = nullptr;
+ WP<CWLPointerResource> m_pointer;
+
+ // constraints
+ DRMFormat m_format = 0 /* DRM_FORMAT_INVALID */;
+ Vector2D m_hotspot = Vector2D(0, 0);
+ Vector2D m_bufferSize = Vector2D(0, 0);
+
+ struct {
+ bool pending = false;
+ PHLMONITOR monitor;
+ SP<IHLBuffer> buffer;
+ FSourceBoxCallback sourceBoxCallback;
+ FScreenshareCallback callback;
+ } m_pendingFrame;
+
+ struct {
+ CHyprSignalListener pointerDestroyed;
+ CHyprSignalListener cursorChanged;
+ } m_listeners;
+
+ bool copy();
+ void render();
+ void calculateConstraints();
+
+ friend class CScreenshareFrame;
+ friend class CScreenshareManager;
+ };
+
+ class CScreenshareFrame {
+ public:
+ CScreenshareFrame(const CScreenshareFrame&) = delete;
+ CScreenshareFrame(CScreenshareFrame&&) = delete;
+ CScreenshareFrame(WP<CScreenshareSession> session, bool overlayCursor, bool isFirst);
+ ~CScreenshareFrame();
+
+ bool done() const;
+ eScreenshareError share(SP<IHLBuffer> buffer, const CRegion& damage, FScreenshareCallback callback);
+
+ Vector2D bufferSize() const;
+ wl_output_transform transform() const; // returns the transform applied by compositor on the buffer
+ const CRegion& damage() const;
+
+ private:
+ WP<CScreenshareFrame> m_self;
+ WP<CScreenshareSession> m_session;
+ FScreenshareCallback m_callback;
+ SP<IHLBuffer> m_buffer;
+ Vector2D m_bufferSize = Vector2D(0, 0);
+ CRegion m_damage; // damage in buffer coords
+ bool m_shared = false, m_copied = false, m_failed = false;
+ bool m_overlayCursor = true;
+ bool m_isFirst = false;
+
+ //
+ void copy();
+ bool copyDmabuf();
+ bool copyShm();
+
+ void render();
+ void renderMonitor();
+ void renderMonitorRegion();
+ void renderWindow();
+
+ void storeTempFB();
+
+ friend class CScreenshareManager;
+ friend class CScreenshareSession;
+ };
+
+ class CScreenshareManager {
+ public:
+ CScreenshareManager();
+
+ UP<CScreenshareSession> newSession(wl_client* client, PHLMONITOR monitor);
+ UP<CScreenshareSession> newSession(wl_client* client, PHLMONITOR monitor, CBox captureRegion);
+ UP<CScreenshareSession> newSession(wl_client* client, PHLWINDOW window);
+
+ WP<CScreenshareSession> getManagedSession(wl_client* client, PHLMONITOR monitor);
+ WP<CScreenshareSession> getManagedSession(wl_client* client, PHLMONITOR monitor, CBox captureBox);
+ WP<CScreenshareSession> getManagedSession(wl_client* client, PHLWINDOW window);
+
+ UP<CCursorshareSession> newCursorSession(wl_client* client, WP<CWLPointerResource> pointer);
+
+ void destroyClientSessions(wl_client* client);
+
+ void onOutputCommit(PHLMONITOR monitor);
+
+ private:
+ std::vector<WP<CScreenshareSession>> m_sessions;
+ std::vector<WP<CCursorshareSession>> m_cursorSessions;
+ std::vector<WP<CScreenshareFrame>> m_pendingFrames;
+
+ struct SManagedSession {
+ SManagedSession(UP<CScreenshareSession>&& session);
+
+ UP<CScreenshareSession> m_session;
+ CHyprSignalListener stoppedListener;
+ };
+
+ std::vector<UP<SManagedSession>> m_managedSessions;
+ WP<CScreenshareSession> getManagedSession(eScreenshareType type, wl_client* client, PHLMONITOR monitor, PHLWINDOW window, CBox captureBox);
+
+ friend class CScreenshareSession;
+ };
+
+ inline UP<CScreenshareManager>& mgr() {
+ static UP<CScreenshareManager> manager = nullptr;
+ if (!manager && g_pHyprRenderer) {
+ Log::logger->log(Log::DEBUG, "Starting ScreenshareManager");
+ manager = makeUnique<CScreenshareManager>();
+ }
+ return manager;
+ }
+}
+
+template <>
+struct std::formatter<Screenshare::eScreenshareType> : std::formatter<std::string> {
+ auto format(const Screenshare::eScreenshareType& res, std::format_context& ctx) const {
+ switch (res) {
+ case Screenshare::SHARE_MONITOR: return formatter<string>::format("monitor", ctx);
+ case Screenshare::SHARE_WINDOW: return formatter<string>::format("window", ctx);
+ case Screenshare::SHARE_REGION: return formatter<string>::format("region", ctx);
+ case Screenshare::SHARE_NONE: return formatter<string>::format("ERR NONE", ctx);
+ }
+ return formatter<string>::format("error", ctx);
+ }
+};
diff --git a/src/managers/screenshare/ScreenshareSession.cpp b/src/managers/screenshare/ScreenshareSession.cpp
new file mode 100644
index 00000000..83402abf
--- /dev/null
+++ b/src/managers/screenshare/ScreenshareSession.cpp
@@ -0,0 +1,162 @@
+#include "ScreenshareManager.hpp"
+#include "../../render/OpenGL.hpp"
+#include "../../Compositor.hpp"
+#include "../../render/Renderer.hpp"
+#include "../HookSystemManager.hpp"
+#include "../EventManager.hpp"
+#include "../eventLoop/EventLoopManager.hpp"
+
+using namespace Screenshare;
+
+CScreenshareSession::CScreenshareSession(PHLMONITOR monitor, wl_client* client) : m_type(SHARE_MONITOR), m_monitor(monitor), m_client(client) {
+ init();
+}
+
+CScreenshareSession::CScreenshareSession(PHLWINDOW window, wl_client* client) : m_type(SHARE_WINDOW), m_window(window), m_client(client) {
+ m_listeners.windowDestroyed = m_window->m_events.unmap.listen([this]() { stop(); });
+ m_listeners.windowSizeChanged = m_window->m_events.resize.listen([this]() {
+ calculateConstraints();
+ m_events.constraintsChanged.emit();
+ });
+ m_listeners.windowMonitorChanged = m_window->m_events.monitorChanged.listen([this]() {
+ m_listeners.monitorDestroyed = monitor()->m_events.disconnect.listen([this]() { stop(); });
+ m_listeners.monitorModeChanged = monitor()->m_events.modeChanged.listen([this]() {
+ calculateConstraints();
+ m_events.constraintsChanged.emit();
+ });
+
+ calculateConstraints();
+ m_events.constraintsChanged.emit();
+ });
+
+ init();
+}
+
+CScreenshareSession::CScreenshareSession(PHLMONITOR monitor, CBox captureRegion, wl_client* client) :
+ m_type(SHARE_REGION), m_monitor(monitor), m_captureBox(captureRegion), m_client(client) {
+ init();
+}
+
+CScreenshareSession::~CScreenshareSession() {
+ stop();
+ LOGM(Log::TRACE, "Destroyed screenshare session for ({}): {}", m_type, m_name);
+}
+
+void CScreenshareSession::stop() {
+ if (m_stopped)
+ return;
+ m_stopped = true;
+ m_events.stopped.emit();
+
+ screenshareEvents(false);
+}
+
+void CScreenshareSession::init() {
+ m_shareStopTimer = makeShared<CEventLoopTimer>(
+ std::chrono::milliseconds(500),
+ [this](SP<CEventLoopTimer> self, void* data) {
+ // if this fires, then it's been half a second since the last frame, so we aren't sharing
+ screenshareEvents(false);
+ },
+ nullptr);
+
+ if (g_pEventLoopManager)
+ g_pEventLoopManager->addTimer(m_shareStopTimer);
+
+ // scale capture box since it's in logical coords
+ m_captureBox.scale(monitor()->m_scale);
+
+ m_listeners.monitorDestroyed = monitor()->m_events.disconnect.listen([this]() { stop(); });
+ m_listeners.monitorModeChanged = monitor()->m_events.modeChanged.listen([this]() {
+ calculateConstraints();
+ m_events.constraintsChanged.emit();
+ });
+
+ calculateConstraints();
+}
+
+void CScreenshareSession::calculateConstraints() {
+ const auto PMONITOR = monitor();
+ if (!PMONITOR) {
+ stop();
+ return;
+ }
+
+ // TODO: maybe support more that just monitor format in the future?
+ m_formats.clear();
+ m_formats.push_back(NFormatUtils::alphaFormat(g_pHyprOpenGL->getPreferredReadFormat(PMONITOR)));
+ m_formats.push_back(g_pHyprOpenGL->getPreferredReadFormat(PMONITOR)); // some clients don't like alpha formats
+
+ // TODO: hack, we can't bit flip so we'll format flip heh, GL_BGRA_EXT won't work here
+ for (auto& format : m_formats) {
+ if (format == DRM_FORMAT_XRGB2101010 || format == DRM_FORMAT_ARGB2101010)
+ format = DRM_FORMAT_XBGR2101010;
+ }
+
+ switch (m_type) {
+ case SHARE_MONITOR:
+ m_bufferSize = PMONITOR->m_pixelSize;
+ m_name = PMONITOR->m_name;
+ break;
+ case SHARE_WINDOW:
+ m_bufferSize = m_window->m_realSize->value().round();
+ m_name = m_window->m_title;
+ break;
+ case SHARE_REGION:
+ m_bufferSize = PMONITOR->m_transform % 2 == 0 ? m_captureBox.size() : Vector2D{m_captureBox.h, m_captureBox.w};
+ m_name = PMONITOR->m_name;
+ break;
+ case SHARE_NONE:
+ default:
+ LOGM(Log::ERR, "Invalid share type?? This shouldn't happen");
+ stop();
+ return;
+ }
+
+ LOGM(Log::TRACE, "constraints changed for {}", m_name);
+}
+
+void CScreenshareSession::screenshareEvents(bool startSharing) {
+ if (startSharing && !m_sharing) {
+ m_sharing = true;
+ EMIT_HOOK_EVENT("screencast", (std::vector<std::any>{1, m_type}));
+ g_pEventManager->postEvent(SHyprIPCEvent{.event = "screencast", .data = std::format("1,{}", m_type)});
+ EMIT_HOOK_EVENT("screencastv2", (std::vector<std::any>{1, m_type, m_name}));
+ g_pEventManager->postEvent(SHyprIPCEvent{.event = "screencastv2", .data = std::format("1,{},{}", m_type, m_name)});
+ LOGM(Log::INFO, "New screenshare session for ({}): {}", m_type, m_name);
+ } else if (!startSharing && m_sharing) {
+ m_sharing = false;
+ EMIT_HOOK_EVENT("screencast", (std::vector<std::any>{0, m_type}));
+ g_pEventManager->postEvent(SHyprIPCEvent{.event = "screencast", .data = std::format("0,{}", m_type)});
+ EMIT_HOOK_EVENT("screencastv2", (std::vector<std::any>{0, m_type, m_name}));
+ g_pEventManager->postEvent(SHyprIPCEvent{.event = "screencastv2", .data = std::format("0,{},{}", m_type, m_name)});
+ LOGM(Log::INFO, "Stopped screenshare session for ({}): {}", m_type, m_name);
+ }
+}
+
+const std::vector<DRMFormat>& CScreenshareSession::allowedFormats() const {
+ return m_formats;
+}
+
+Vector2D CScreenshareSession::bufferSize() const {
+ return m_bufferSize;
+}
+
+PHLMONITOR CScreenshareSession::monitor() const {
+ if (m_type == SHARE_WINDOW && m_window.expired())
+ return nullptr;
+ PHLMONITORREF mon = m_type == SHARE_WINDOW ? m_window->m_monitor : m_monitor;
+ return mon.expired() ? nullptr : mon.lock();
+}
+
+UP<CScreenshareFrame> CScreenshareSession::nextFrame(bool overlayCursor) {
+ UP<CScreenshareFrame> frame = makeUnique<CScreenshareFrame>(m_self, overlayCursor, !m_sharing);
+ frame->m_self = frame;
+
+ Screenshare::mgr()->m_pendingFrames.emplace_back(frame);
+
+ // there is now a pending frame, so block ds
+ g_pHyprRenderer->m_directScanoutBlocked = true;
+
+ return frame;
+}