Remove garbage GPTed readme

This commit is contained in:
Stephen Hellings 2026-03-10 13:11:57 -04:00
parent 104261d74c
commit 0bede114a6
15 changed files with 499 additions and 189 deletions

1
Cargo.lock generated
View file

@ -3209,6 +3209,7 @@ dependencies = [
"udev",
"wayland-backend",
"wayland-protocols",
"wayland-protocols-wlr",
"wayland-server",
"xcursor",
"xkbcommon 0.9.0",

View file

@ -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

14
electron/dist/main.js vendored
View file

@ -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);
}

View file

@ -1219,6 +1219,19 @@ function connectToCompositor(): Promise<void> {
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);

View file

@ -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 = {

View file

@ -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<string, string>;
}
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

View file

@ -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<WoWindow[]>([]);
const [metadata, setMetadata] = useState<WindowMetadataEvent | null>(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 (
<div>
<p>Active windows: {metadata?.count ?? 0}</p>
</div>
);
}
```
## API Reference
### `CompositorAPI`
Exposed as `window.compositor` in Electron windows.
#### Methods
- `action(name: string, payload?: Record<string, unknown>): Promise<unknown>`
- Send an action to the compositor
- `actionSync(name: string, payload?: Record<string, unknown>): void`
- Send an action synchronously (fire and forget)
- `quit(code?: number): Promise<unknown>`
- Request the compositor to quit
- `reload(): Promise<unknown>`
- Reload the current window
- `toggleDevTools(): Promise<unknown>`
- 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<string, unknown>): Promise<unknown>`
- Execute a syscall in the compositor
### `WoClientAPI`
Exposed as `window.woClient` in Electron windows.
#### Methods
- `getWindows(): Promise<WoWindow[]>`
- 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

View file

@ -21,6 +21,8 @@ export type {
DamagePatch,
RendererDamageHelper,
RendererDamageHelperOptions,
PortalRequestEvent,
ScreencopyEvent,
} from './types';
export { InteractionDispatcher } from './dispatcher';

View file

@ -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"
},

View file

@ -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 */

View file

@ -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

View file

@ -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;

252
src/handlers/screencopy.rs Normal file
View file

@ -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<Mutex<Vec<PendingFrame>>>,
}
/// 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<D>(display: &DisplayHandle) -> Self
where
D: GlobalDispatch<ZwlrScreencopyManagerV1, ()> + 'static,
D: Dispatch<ZwlrScreencopyManagerV1, ()> + 'static,
D: Dispatch<ZwlrScreencopyFrameV1, ScreencopyFrameState> + 'static,
{
display.create_global::<D, ZwlrScreencopyManagerV1, ()>(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<PendingFrame> {
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<ZwlrScreencopyManagerV1, ()> for WoState {
fn bind(
_state: &mut Self,
_handle: &DisplayHandle,
_client: &Client,
resource: New<ZwlrScreencopyManagerV1>,
_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<ZwlrScreencopyManagerV1, ()> 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<ZwlrScreencopyFrameV1, ScreencopyFrameState> 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();
}
}

View file

@ -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);

View file

@ -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<ElectronIpc>,
pub syscall_handler: Option<SyscallHandler>,
pub portal: Option<std::sync::Arc<WoPortal>>,
/// wlr-screencopy protocol state for frame capture requests.
pub screencopy_state: Option<ScreencopyManagerState>,
/// Mapping state for named Electron windows. If false, the window should
/// not be rendered (unmapped/minimized).
pub window_mapped: std::collections::HashMap<String, bool>,
@ -343,6 +346,9 @@ impl WoState {
None
};
// Initialize wlr-screencopy protocol
let screencopy_state = Some(ScreencopyManagerState::new::<Self>(&dh));
let data_device_state = DataDeviceState::new::<Self>(&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,