Remove garbage GPTed readme
This commit is contained in:
parent
104261d74c
commit
0bede114a6
15 changed files with 499 additions and 189 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3209,6 +3209,7 @@ dependencies = [
|
|||
"udev",
|
||||
"wayland-backend",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-wlr",
|
||||
"wayland-server",
|
||||
"xcursor",
|
||||
"xkbcommon 0.9.0",
|
||||
|
|
|
|||
|
|
@ -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
14
electron/dist/main.js
vendored
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -21,6 +21,8 @@ export type {
|
|||
DamagePatch,
|
||||
RendererDamageHelper,
|
||||
RendererDamageHelperOptions,
|
||||
PortalRequestEvent,
|
||||
ScreencopyEvent,
|
||||
} from './types';
|
||||
|
||||
export { InteractionDispatcher } from './dispatcher';
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
11
electron/types/types.d.ts
vendored
11
electron/types/types.d.ts
vendored
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
252
src/handlers/screencopy.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
145
src/main.rs
145
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue