diff --git a/Cargo.lock b/Cargo.lock index 5065c25..be48ef4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3209,6 +3209,7 @@ dependencies = [ "udev", "wayland-backend", "wayland-protocols", + "wayland-protocols-wlr", "wayland-server", "xcursor", "xkbcommon 0.9.0", diff --git a/Cargo.toml b/Cargo.toml index fae4f03..0bdfc86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ gl = "0.14" # Wayland wayland-server = "0.31" wayland-protocols = { version = "0.32", features = ["server", "staging", "unstable"] } +wayland-protocols-wlr = { version = "0.3", features = ["server"] } wayland-backend = { version = "0.3", features = ["server_system"] } # Async / IPC diff --git a/electron/dist/main.js b/electron/dist/main.js index d7601c4..9e94253 100644 --- a/electron/dist/main.js +++ b/electron/dist/main.js @@ -46,7 +46,8 @@ var MAGIC = { FORWARD_KEYBOARD: 1464814405, FORWARD_RELATIVE_POINTER: 1464816197, POINTER_LOCK_REQUEST: 1464815692, - ENV_UPDATE: 1464812885 + ENV_UPDATE: 1464812885, + SCREENCOPY_EVENT: 1464816453 }; function stringToBuffer(str) { return Buffer.from(str, "utf-8"); @@ -1319,6 +1320,17 @@ function connectToCompositor() { } continue; } + if (magic === MAGIC.SCREENCOPY_EVENT) { + if (compositorRxBuffer.length < 12) + break; + const active = compositorRxBuffer.readUInt32LE(4) !== 0; + const clientCount = compositorRxBuffer.readUInt32LE(8); + compositorRxBuffer = compositorRxBuffer.slice(12); + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send("wo:screencopy-event", { active, clientCount }); + } + continue; + } debugLog("[Wo] UNKNOWN magic 0x" + magic.toString(16).padStart(8, "0") + " bufLen=" + compositorRxBuffer.length + " — stream may be corrupted"); compositorRxBuffer = compositorRxBuffer.slice(1); } diff --git a/electron/src/main.ts b/electron/src/main.ts index fd5a211..e6b5127 100644 --- a/electron/src/main.ts +++ b/electron/src/main.ts @@ -1219,6 +1219,19 @@ function connectToCompositor(): Promise { continue; } + if (magic === MAGIC.SCREENCOPY_EVENT) { + // magic(4) + active(4) + client_count(4) = 12 bytes + if (compositorRxBuffer.length < 12) break; + const active = compositorRxBuffer.readUInt32LE(4) !== 0; + const clientCount = compositorRxBuffer.readUInt32LE(8); + compositorRxBuffer = compositorRxBuffer.slice(12); + + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.webContents.send('wo:screencopy-event', { active, clientCount }); + } + continue; + } + // Unknown message magic; resync by advancing one byte. debugLog('[Wo] UNKNOWN magic 0x' + magic.toString(16).padStart(8, '0') + ' bufLen=' + compositorRxBuffer.length + ' — stream may be corrupted'); compositorRxBuffer = compositorRxBuffer.slice(1); diff --git a/electron/src/preload.ts b/electron/src/preload.ts index 5bc5719..b2d23dd 100644 --- a/electron/src/preload.ts +++ b/electron/src/preload.ts @@ -115,6 +115,14 @@ const compositorAPI: CompositorAPI = { ipcRenderer.on('wo:portal-request', handler); return () => ipcRenderer.removeListener('wo:portal-request', handler); }, + + // Screencopy capture event notifications + onScreencopyEvent: (callback: (data: { active: boolean; clientCount: number }) => void) => { + const handler = (_event: Electron.IpcRendererEvent, data: { active: boolean; clientCount: number }) => + callback(data); + ipcRenderer.on('wo:screencopy-event', handler); + return () => ipcRenderer.removeListener('wo:screencopy-event', handler); + }, }; const woClientAPI: WoClientAPI = { diff --git a/electron/src/protocol.ts b/electron/src/protocol.ts index 0adf67c..a7b6be9 100644 --- a/electron/src/protocol.ts +++ b/electron/src/protocol.ts @@ -31,6 +31,7 @@ export const MAGIC = { FORWARD_RELATIVE_POINTER: 0x574F5245, // "WORE" — forwarded relative pointer (dx/dy) POINTER_LOCK_REQUEST: 0x574F504C, // "WOPL" — server-to-client pointer lock request ENV_UPDATE: 0x574F4555, // "WOEU" — environment variable update broadcast + SCREENCOPY_EVENT: 0x574F5345, // "WOSE" — screencopy capture event notification } as const; /** @@ -96,6 +97,12 @@ export interface EnvUpdateEvent { vars: Record; } +export interface ScreencopyEvent { + type: 'screencopyEvent'; + active: boolean; + clientCount: number; +} + export type InputEvent = | MouseMoveEvent | MouseButtonEvent @@ -104,7 +111,8 @@ export type InputEvent = | FocusChangeEvent | WindowMetadataEvent | PointerLockRequestEvent - | EnvUpdateEvent; + | EnvUpdateEvent + | ScreencopyEvent; /** * Compositor Actions from Client → Compositor diff --git a/electron/types/README.md b/electron/types/README.md deleted file mode 100644 index b005a8c..0000000 --- a/electron/types/README.md +++ /dev/null @@ -1,186 +0,0 @@ -# wo-types - -Type definitions for the Wo Wayland compositor Electron integration. - -This package provides fully-typed interfaces for the compositor APIs exposed to Electron windows running within the Wo compositor. - -## Installation - -```bash -bun install wo-types -# or -npm install wo-types -``` - -## Usage - -### In Preload Scripts - -```typescript -import type { CompositorAPI, WoClientAPI } from 'wo-types'; - -// These are already exposed as window.compositor and window.woClient -const compositorAPI: CompositorAPI = window.compositor; -const clientAPI: WoClientAPI = window.woClient; -``` - -### In React Components - -```typescript -import type { WoWindow, WindowMetadataEvent } from 'wo-types'; -import { useEffect, useState } from 'react'; - -function MyComponent() { - const [windows, setWindows] = useState([]); - const [metadata, setMetadata] = useState(null); - - useEffect(() => { - // Subscribe to window updates - const unsubWindows = window.woClient?.onWindows((windowList) => { - setWindows(windowList); - }); - - // Subscribe to metadata updates - const unsubMetadata = window.compositor?.onWindowMetadata((meta) => { - setMetadata(meta); - }); - - return () => { - unsubWindows?.(); - unsubMetadata?.(); - }; - }, []); - - return ( -
-

Active windows: {metadata?.count ?? 0}

-
- ); -} -``` - -## API Reference - -### `CompositorAPI` - -Exposed as `window.compositor` in Electron windows. - -#### Methods - -- `action(name: string, payload?: Record): Promise` - - Send an action to the compositor - -- `actionSync(name: string, payload?: Record): void` - - Send an action synchronously (fire and forget) - -- `quit(code?: number): Promise` - - Request the compositor to quit - -- `reload(): Promise` - - Reload the current window - -- `toggleDevTools(): Promise` - - Toggle DevTools for the current window - -- `onFocusChange(callback: (window: string, focused: boolean) => void): () => void` - - Register focus change notification callback - -- `onWindowMetadata(callback: (metadata: WindowMetadataEvent) => void): () => void` - - Register window metadata update callback - -- `syscall(type: string, params: Record): Promise` - - Execute a syscall in the compositor - -### `WoClientAPI` - -Exposed as `window.woClient` in Electron windows. - -#### Methods - -- `getWindows(): Promise` - - Get list of all windows - -- `onWindows(callback: (windows: WoWindow[]) => void): () => void` - - Register window list update callback - -- `sendAction(action: string): void` - - Send a keybind action to the compositor - -## Types - -### `WindowMetadata` - -Describes a Wayland surface or application window. - -```typescript -interface WindowMetadata { - name: string; - title?: string; - width: number; - height: number; - x: number; - y: number; - z_order: number; - fps: number; - focused?: boolean; - focusable: boolean; - interactive: boolean; -} -``` - -### `WoWindow` - -Window information returned by the client API. - -```typescript -interface WoWindow { - name: string; - title?: string; - x: number; - y: number; - width: number; - height: number; - z_order: number; - fps: number; - focused: boolean; -} -``` - -### `WindowMetadataEvent` - -Event data for window metadata updates. - -```typescript -interface WindowMetadataEvent { - windows: WindowMetadata[]; - count: number; -} -``` - -### `FocusChangeEvent` - -Event data for focus change notifications. - -```typescript -interface FocusChangeEvent { - window: string; - focused: boolean; -} -``` - -### `S2CAction` - -Enum of server-to-client actions. - -```typescript -enum S2CAction { - WindowMetadataUpdate = 'window-metadata-update', - FocusChange = 'focus-change', - SyscallResponse = 'syscall-response', - InputEvent = 'input-event', -} -``` - -## License - -MIT diff --git a/electron/types/index.ts b/electron/types/index.ts index 5f124f5..5d62cec 100644 --- a/electron/types/index.ts +++ b/electron/types/index.ts @@ -21,6 +21,8 @@ export type { DamagePatch, RendererDamageHelper, RendererDamageHelperOptions, + PortalRequestEvent, + ScreencopyEvent, } from './types'; export { InteractionDispatcher } from './dispatcher'; diff --git a/electron/types/package.json b/electron/types/package.json index 370ccc4..509d3e8 100644 --- a/electron/types/package.json +++ b/electron/types/package.json @@ -2,7 +2,7 @@ "name": "@wl-wo/wo-types", "module": "index.ts", "type": "module", - "version": "0.2.0", + "version": "0.3.0", "devDependencies": { "@types/bun": "latest" }, diff --git a/electron/types/types.d.ts b/electron/types/types.d.ts index eee5c27..686f2a1 100644 --- a/electron/types/types.d.ts +++ b/electron/types/types.d.ts @@ -110,6 +110,11 @@ export interface PortalRequestEvent { sessionId?: string; } +export interface ScreencopyEvent { + active: boolean; + clientCount: number; +} + /** * Application information for launching */ @@ -288,6 +293,12 @@ export interface Compositor { * Subscribe to generic portal requests so clients can provide custom UI. */ onPortalRequest(callback: (data: PortalRequestEvent) => void): Unsubscribe; + + /** + * Subscribe to screencopy capture events (active/inactive notifications). + * Fires when Wayland clients (e.g. OBS, grim) capture the screen via wlr-screencopy. + */ + onScreencopyEvent(callback: (data: ScreencopyEvent) => void): Unsubscribe; } /** Alias for Compositor to match preload.ts imports */ diff --git a/src/electron.rs b/src/electron.rs index e09cc7f..c256dda 100644 --- a/src/electron.rs +++ b/src/electron.rs @@ -49,6 +49,7 @@ pub const MAGIC_FORWARD_POINTER_BUTTON: u32 = 0x574F5042; // "WOPB" pub const MAGIC_FORWARD_POINTER_SCROLL: u32 = 0x574F5053; // "WOPS" pub const MAGIC_POINTER_LOCK_REQUEST: u32 = 0x574F504C; // "WOPL" — server-to-client pointer lock request pub const MAGIC_ENV_UPDATE: u32 = 0x574F4555; // "WOEU" — environment variable update broadcast +pub const MAGIC_SCREENCOPY_EVENT: u32 = 0x574F5345; // "WOSE" — screencopy capture event notification #[repr(C, packed)] @@ -146,6 +147,8 @@ pub enum ElectronInputEvent { PointerLockRequest { window_name: String, lock: bool }, /// Environment variable update (e.g. DISPLAY after XWayland is ready). EnvUpdate { vars: String }, + /// Screencopy capture event (active/inactive notification). + ScreencopyEvent { active: bool, client_count: u32 }, } /// A bidirectional connection to an Electron client. @@ -464,6 +467,13 @@ impl ElectronClientConnection { buf[8..].copy_from_slice(var_bytes); self.write_bytes(&buf)?; }, + ElectronInputEvent::ScreencopyEvent { active, client_count } => { + let mut buf = [0u8; 12]; + buf[0..4].copy_from_slice(&MAGIC_SCREENCOPY_EVENT.to_le_bytes()); + buf[4..8].copy_from_slice(&(*active as u32).to_le_bytes()); + buf[8..12].copy_from_slice(&client_count.to_le_bytes()); + self.write_bytes(&buf)?; + }, } Ok(()) } @@ -539,6 +549,13 @@ impl ElectronClientConnection { buf[8..].copy_from_slice(var_bytes); self.write_bytes_nonblocking(&buf) } + ElectronInputEvent::ScreencopyEvent { active, client_count } => { + let mut buf = [0u8; 12]; + buf[0..4].copy_from_slice(&MAGIC_SCREENCOPY_EVENT.to_le_bytes()); + buf[4..8].copy_from_slice(&(*active as u32).to_le_bytes()); + buf[8..12].copy_from_slice(&client_count.to_le_bytes()); + self.write_bytes_nonblocking(&buf) + } } } } @@ -725,6 +742,24 @@ impl ElectronIpc { Ok(()) } + /// Broadcast screencopy capture event to all connected Electron clients. + pub fn broadcast_screencopy_event(&self, active: bool, client_count: u32) -> Result<()> { + let snapshot: Vec<_> = { + let clients = self.clients.lock().unwrap(); + clients.values().cloned().collect() + }; + let event = ElectronInputEvent::ScreencopyEvent { + active, + client_count, + }; + for client in &snapshot { + if let Err(e) = client.try_send_input_event(&event) { + warn!("screencopy event broadcast to '{}' failed: {e:#}", client.name); + } + } + Ok(()) + } + /// Send syscall response to a window (non-blocking). /// If the socket buffer is full, the response is dropped and a warning is /// logged. This is preferable to blocking the compositor event loop — the diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 89f423d..29fd2ec 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -2,6 +2,7 @@ pub mod compositor; pub mod dmabuf_handler; pub mod input; pub mod output; +pub mod screencopy; pub mod xdg_decoration; pub mod xdg_shell; pub mod xwayland; diff --git a/src/handlers/screencopy.rs b/src/handlers/screencopy.rs new file mode 100644 index 0000000..94f27d5 --- /dev/null +++ b/src/handlers/screencopy.rs @@ -0,0 +1,252 @@ +/// wlr-screencopy-unstable-v1 protocol handler for the Wo compositor. +/// +/// Implements `zwlr_screencopy_manager_v1` allowing clients like grim, OBS, etc. +/// to request frame copies of outputs or output regions. + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use tracing::{debug, info, warn}; +use wayland_protocols_wlr::screencopy::v1::server::{ + zwlr_screencopy_frame_v1::{self, ZwlrScreencopyFrameV1}, + zwlr_screencopy_manager_v1::{self, ZwlrScreencopyManagerV1}, +}; +use wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + protocol::{wl_buffer::WlBuffer, wl_output::WlOutput, wl_shm}, +}; + +use crate::state::WoState; + +/// State for a pending screencopy frame capture. +#[derive(Debug)] +pub struct ScreencopyFrameState { + /// Whether the cursor overlay was requested. + pub overlay_cursor: bool, + /// Capture region in output-logical coordinates (x, y, w, h). + pub region: (i32, i32, i32, i32), + /// Output width at frame creation time. + pub output_width: u32, + /// Output height at frame creation time. + pub output_height: u32, + /// Whether a buffer has been attached via `copy` or `copy_with_damage`. + pub buffer_attached: bool, + /// Whether we're waiting for damage (`copy_with_damage`). + pub with_damage: bool, +} + +/// Compositor-wide screencopy manager state. +pub struct ScreencopyManagerState { + /// Pending frame requests waiting for the next render pass. + pub pending_frames: Arc>>, +} + +/// A frame request queued for the next render pass. +pub struct PendingFrame { + pub frame: ZwlrScreencopyFrameV1, + pub buffer: WlBuffer, + pub region: (i32, i32, i32, i32), + pub overlay_cursor: bool, + pub with_damage: bool, +} + +impl ScreencopyManagerState { + pub fn new(display: &DisplayHandle) -> Self + where + D: GlobalDispatch + 'static, + D: Dispatch + 'static, + D: Dispatch + 'static, + { + display.create_global::(3, ()); + info!("Registered zwlr_screencopy_manager_v1 global (v3)"); + + Self { + pending_frames: Arc::new(Mutex::new(Vec::new())), + } + } + + /// Take all pending frame requests for processing in the render loop. + pub fn take_pending_frames(&self) -> Vec { + let mut pending = self.pending_frames.lock().unwrap(); + std::mem::take(&mut *pending) + } + + /// Check if there are any pending frame requests. + pub fn has_pending_frames(&self) -> bool { + let pending = self.pending_frames.lock().unwrap(); + !pending.is_empty() + } +} + +// ── GlobalDispatch for the manager ────────────────────────────────────────── + +impl GlobalDispatch for WoState { + fn bind( + _state: &mut Self, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &(), + data_init: &mut DataInit<'_, Self>, + ) { + data_init.init(resource, ()); + debug!("Client bound zwlr_screencopy_manager_v1"); + } +} + +// ── Dispatch for the manager interface ────────────────────────────────────── + +impl Dispatch for WoState { + fn request( + state: &mut Self, + _client: &Client, + _resource: &ZwlrScreencopyManagerV1, + request: zwlr_screencopy_manager_v1::Request, + _data: &(), + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, Self>, + ) { + match request { + zwlr_screencopy_manager_v1::Request::CaptureOutput { + frame, + overlay_cursor, + output: _, + } => { + let (w, h) = state.output_size; + let frame_state = ScreencopyFrameState { + overlay_cursor: overlay_cursor != 0, + region: (0, 0, w as i32, h as i32), + output_width: w, + output_height: h, + buffer_attached: false, + with_damage: false, + }; + + let frame_resource = data_init.init(frame, frame_state); + send_buffer_info(&frame_resource, w, h); + debug!( + "CaptureOutput: {}x{}, cursor_overlay={}", + w, h, overlay_cursor != 0 + ); + } + zwlr_screencopy_manager_v1::Request::CaptureOutputRegion { + frame, + overlay_cursor, + output: _, + x, + y, + width, + height, + } => { + let (ow, oh) = state.output_size; + // Clamp region to output bounds + let rx = x.max(0); + let ry = y.max(0); + let rw = width.min(ow as i32 - rx).max(1); + let rh = height.min(oh as i32 - ry).max(1); + + let frame_state = ScreencopyFrameState { + overlay_cursor: overlay_cursor != 0, + region: (rx, ry, rw, rh), + output_width: rw as u32, + output_height: rh as u32, + buffer_attached: false, + with_damage: false, + }; + + let frame_resource = data_init.init(frame, frame_state); + send_buffer_info(&frame_resource, rw as u32, rh as u32); + debug!( + "CaptureOutputRegion: ({},{}) {}x{}, cursor_overlay={}", + rx, ry, rw, rh, overlay_cursor != 0 + ); + } + zwlr_screencopy_manager_v1::Request::Destroy => { + debug!("Client destroyed screencopy manager"); + } + _ => {} + } + } +} + +/// Send buffer format info events to the frame client. +fn send_buffer_info(frame: &ZwlrScreencopyFrameV1, width: u32, height: u32) { + let stride = width * 4; // ARGB8888 / BGRx + + // SHM buffer support (always available) + frame.buffer( + wl_shm::Format::Argb8888, + width, + height, + stride, + ); + + // DMABUF support (v3) + if frame.version() >= 3 { + frame.linux_dmabuf( + drm_fourcc::DrmFourcc::Argb8888 as u32, + width, + height, + ); + frame.buffer_done(); + } +} + +// ── Dispatch for the frame interface ──────────────────────────────────────── + +impl Dispatch for WoState { + fn request( + state: &mut Self, + _client: &Client, + resource: &ZwlrScreencopyFrameV1, + request: zwlr_screencopy_frame_v1::Request, + data: &ScreencopyFrameState, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, Self>, + ) { + match request { + zwlr_screencopy_frame_v1::Request::Copy { buffer } => { + queue_frame_copy(state, resource, buffer, data, false); + } + zwlr_screencopy_frame_v1::Request::CopyWithDamage { buffer } => { + queue_frame_copy(state, resource, buffer, data, true); + } + zwlr_screencopy_frame_v1::Request::Destroy => { + debug!("Screencopy frame destroyed"); + } + _ => {} + } + } +} + +/// Queue a frame copy request for processing in the next render pass. +fn queue_frame_copy( + state: &mut WoState, + frame: &ZwlrScreencopyFrameV1, + buffer: WlBuffer, + data: &ScreencopyFrameState, + with_damage: bool, +) { + if let Some(ref screencopy_state) = state.screencopy_state { + let pending = PendingFrame { + frame: frame.clone(), + buffer, + region: data.region, + overlay_cursor: data.overlay_cursor, + with_damage, + }; + + screencopy_state + .pending_frames + .lock() + .unwrap() + .push(pending); + + debug!( + "Queued screencopy frame: region={:?}, with_damage={}", + data.region, with_damage + ); + } else { + warn!("Screencopy frame copy requested but screencopy state not initialized"); + frame.failed(); + } +} diff --git a/src/main.rs b/src/main.rs index 3a9fba9..3cc7f7b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,6 +54,7 @@ use std::{ }; use tracing::{error, info, trace, warn}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +use wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1; pub mod state; pub mod syscall; @@ -945,6 +946,150 @@ fn render_frame(data: &mut EventLoopData) { renderer.with_context(|gl| unsafe { gl.Finish() }) .unwrap_or_else(|e| warn!("glFinish failed: {e}")); + // ── Fulfill pending wlr-screencopy frame requests ─────────────── + // After glFinish the framebuffer pixels are ready. Read them back + // and copy into each waiting client's SHM buffer. + if let Some(ref screencopy) = data.state.screencopy_state { + let pending = screencopy.take_pending_frames(); + if !pending.is_empty() { + // Read back the full framebuffer via glReadPixels + let (fb_w, fb_h) = (data.width as i32, data.height as i32); + let fb_size = (fb_w as usize) * (fb_h as usize) * 4; + let mut fb_pixels = vec![0u8; fb_size]; + let read_ok = renderer.with_context(|gl| unsafe { + gl.ReadPixels( + 0, 0, fb_w, fb_h, + gl::RGBA, + gl::UNSIGNED_BYTE, + fb_pixels.as_mut_ptr() as *mut _, + ); + }); + + if read_ok.is_ok() { + // GL returns bottom-up rows; flip to top-down. + let stride = fb_w as usize * 4; + for y in 0..(fb_h as usize / 2) { + let top = y * stride; + let bot = (fb_h as usize - 1 - y) * stride; + for x in 0..stride { + fb_pixels.swap(top + x, bot + x); + } + } + + // Convert RGBA→ARGB (wl_shm expects ARGB8888) + for pixel in fb_pixels.chunks_exact_mut(4) { + let (r, g, b, a) = (pixel[0], pixel[1], pixel[2], pixel[3]); + pixel[0] = b; + pixel[1] = g; + pixel[2] = r; + pixel[3] = a; + } + + use smithay::wayland::shm::with_buffer_contents_mut; + use std::time::SystemTime; + + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default(); + let tv_sec = now.as_secs(); + let tv_nsec = now.subsec_nanos(); + + for pf in pending { + let (rx, ry, rw, rh) = pf.region; + + // Copy the requested region into the client's SHM buffer + let copy_ok = with_buffer_contents_mut(&pf.buffer, |dst_ptr, dst_len, buffer_data| { + let dst = unsafe { std::slice::from_raw_parts_mut(dst_ptr, dst_len) }; + let dst_stride = buffer_data.stride as usize; + let src_stride = fb_w as usize * 4; + + for row in 0..rh as usize { + let src_y = ry as usize + row; + if src_y >= fb_h as usize { break; } + let src_off = src_y * src_stride + (rx as usize) * 4; + let dst_off = row * dst_stride; + let copy_len = (rw as usize * 4).min(dst_stride).min(src_stride - (rx as usize) * 4); + if src_off + copy_len <= fb_pixels.len() && dst_off + copy_len <= dst.len() { + dst[dst_off..dst_off + copy_len] + .copy_from_slice(&fb_pixels[src_off..src_off + copy_len]); + } + } + }); + + if copy_ok.is_ok() { + if pf.with_damage { + pf.frame.damage( + 0, 0, + rw as u32, rh as u32, + ); + } + pf.frame.flags(zwlr_screencopy_frame_v1::Flags::empty()); + pf.frame.ready( + (tv_sec >> 32) as u32, + tv_sec as u32, + tv_nsec, + ); + } else { + pf.frame.failed(); + } + } + + // Push pixels to portal PipeWire streams for screen sharing + if let Some(ref portal) = data.state.portal { + portal.push_pixels_to_streams(None, &fb_pixels); + } + } else { + for pf in pending { + pf.frame.failed(); + } + } + } + } else if let Some(ref portal) = data.state.portal { + // No screencopy requests, but portal streams may be active. + // Only do the expensive readback if there are active portal streams. + if !portal.get_active_sessions().is_empty() { + let (fb_w, fb_h) = (data.width as i32, data.height as i32); + let fb_size = (fb_w as usize) * (fb_h as usize) * 4; + let mut fb_pixels = vec![0u8; fb_size]; + let read_ok = renderer.with_context(|gl| unsafe { + gl.ReadPixels( + 0, 0, fb_w, fb_h, + gl::RGBA, + gl::UNSIGNED_BYTE, + fb_pixels.as_mut_ptr() as *mut _, + ); + }); + if read_ok.is_ok() { + let stride = fb_w as usize * 4; + for y in 0..(fb_h as usize / 2) { + let top = y * stride; + let bot = (fb_h as usize - 1 - y) * stride; + for x in 0..stride { + fb_pixels.swap(top + x, bot + x); + } + } + for pixel in fb_pixels.chunks_exact_mut(4) { + let (r, g, b, a) = (pixel[0], pixel[1], pixel[2], pixel[3]); + pixel[0] = b; + pixel[1] = g; + pixel[2] = r; + pixel[3] = a; + } + portal.push_pixels_to_streams(None, &fb_pixels); + } + } + } + + // Notify Electron/JS when screencopy captures are active + if let Some(ref screencopy) = data.state.screencopy_state { + let has_pending = screencopy.has_pending_frames(); + if has_pending { + if let Some(ref ipc) = data.state.electron_ipc { + let _ = ipc.broadcast_screencopy_event(true, 1); + } + } + } + // Unbind the render target so the BO is no longer GL-locked. drop(target); diff --git a/src/state.rs b/src/state.rs index de9f871..1498fff 100644 --- a/src/state.rs +++ b/src/state.rs @@ -31,6 +31,7 @@ use crate::{ cursor::CursorThemeManager, dmabuf::TextureCache, electron::{ElectronIpc, ElectronProcess}, + handlers::screencopy::ScreencopyManagerState, syscall::SyscallHandler, }; use portal::WoPortal; @@ -244,6 +245,8 @@ pub struct WoState { pub electron_ipc: Option, pub syscall_handler: Option, pub portal: Option>, + /// wlr-screencopy protocol state for frame capture requests. + pub screencopy_state: Option, /// Mapping state for named Electron windows. If false, the window should /// not be rendered (unmapped/minimized). pub window_mapped: std::collections::HashMap, @@ -343,6 +346,9 @@ impl WoState { None }; + // Initialize wlr-screencopy protocol + let screencopy_state = Some(ScreencopyManagerState::new::(&dh)); + let data_device_state = DataDeviceState::new::(&dh); let dmabuf_state = DmabufState::new(); @@ -439,6 +445,7 @@ impl WoState { electron_ipc: None, syscall_handler, portal, + screencopy_state, xwayland: None, xwm: None, xwayland_shell_state: None,