Skip to content

Commit d2a327b

Browse files
westgatewestgate
authored andcommitted
feat(display): wire Phase 2 IPC — present, subscribe_input, poll_events
Implements the three unimplemented display IPC methods from PG-42 (primalSpring Display Phase 2 handoff): - display.present: accepts pixel data via base64 inline or shared memory path, writes to window's DRM dumb buffer via with_mapping - display.subscribe_input: sets server-side input focus to a window - display.poll_events: drains buffered evdev events non-blockingly Supporting changes: - WindowManager::present_window() writes raw pixels to a window's framebuffer via DRM mapping - InputManager::poll_events() now drains the mpsc channel with try_recv instead of returning empty Vec - InputManager::empty() constructor for fallback when discovery fails - DisplayServer integrates InputManager, passes to dispatch - DisplayClient gains present(), present_shm(), subscribe_input(), poll_events() matching the server-side methods - DisplayMethod::Present expanded with data/shm_path fields - 8 new tests for Phase 2 dispatch + InputManager drain behavior petalTongue can now render through toadStool's framebuffer and receive input events over JSON-RPC IPC. Made-with: Cursor
1 parent 553636c commit d2a327b

8 files changed

Lines changed: 465 additions & 39 deletions

File tree

crates/runtime/display/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ futures = { workspace = true }
3131
tarpc = { workspace = true }
3232
serde = { workspace = true }
3333
serde_json = { workspace = true }
34+
base64 = { workspace = true }
3435

3536
# Error handling
3637
thiserror = { workspace = true }

crates/runtime/display/src/input/mod.rs

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ pub struct InputManager {
6666
}
6767

6868
impl InputManager {
69+
/// Create an empty input manager with no devices (fallback when discovery fails).
70+
#[must_use]
71+
pub fn empty() -> Self {
72+
let (event_tx, event_rx) = mpsc::channel(1000);
73+
Self {
74+
devices: vec![],
75+
shared_focus: Arc::new(RwLock::new(None)),
76+
event_tx,
77+
event_rx: Some(event_rx),
78+
}
79+
}
80+
6981
/// Discover and initialize input devices
7082
///
7183
/// **Self-knowledge**: Discovers own hardware at runtime!
@@ -214,19 +226,25 @@ impl InputManager {
214226
)
215227
}
216228

217-
/// Poll for input events
229+
/// Poll for input events (non-blocking).
218230
///
219-
/// Non-blocking check for pending events.
231+
/// Drains all pending events from the internal channel and returns them.
232+
/// Returns an empty `Vec` when no events are queued. Safe to call in a
233+
/// tight loop — uses `try_recv` internally.
220234
///
221-
/// Note: This is a simplified API. For streaming, use `subscribe_events()`.
235+
/// For streaming, use `subscribe_events()` instead.
222236
///
223237
/// # Errors
224238
///
225239
/// Currently always returns `Ok`; reserved for future error cases.
226-
pub const fn poll_events(&mut self) -> Result<Vec<InputEvent>> {
227-
// For now, return empty - real streaming happens via subscribe_events()
228-
// This is here for compatibility with the old API
229-
Ok(Vec::new())
240+
pub fn poll_events(&mut self) -> Result<Vec<InputEvent>> {
241+
let mut events = Vec::new();
242+
if let Some(rx) = &mut self.event_rx {
243+
while let Ok(event) = rx.try_recv() {
244+
events.push(event);
245+
}
246+
}
247+
Ok(events)
230248
}
231249

232250
/// Route events to a specific window
@@ -341,4 +359,40 @@ mod tests {
341359

342360
assert!(received.is_some());
343361
}
362+
363+
#[tokio::test]
364+
async fn test_poll_events_drains_channel() {
365+
let mut manager = InputManager::empty();
366+
let window = WindowId::new();
367+
368+
let event_a = InputEvent::KeyPress {
369+
key: KeyCode::A,
370+
modifiers: Modifiers::default(),
371+
window,
372+
};
373+
let event_b = InputEvent::KeyRelease {
374+
key: KeyCode::A,
375+
modifiers: Modifiers::default(),
376+
window,
377+
};
378+
379+
manager.inject_event(event_a).await.unwrap();
380+
manager.inject_event(event_b).await.unwrap();
381+
382+
let polled = manager.poll_events().unwrap();
383+
assert_eq!(polled.len(), 2, "should drain both events");
384+
385+
let polled_again = manager.poll_events().unwrap();
386+
assert!(
387+
polled_again.is_empty(),
388+
"channel should be empty after drain"
389+
);
390+
}
391+
392+
#[tokio::test]
393+
async fn test_empty_manager_poll_returns_empty() {
394+
let mut manager = InputManager::empty();
395+
let events = manager.poll_events().unwrap();
396+
assert!(events.is_empty());
397+
}
344398
}

crates/runtime/display/src/ipc/client/operations.rs

Lines changed: 122 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
// SPDX-License-Identifier: AGPL-3.0-or-later
2-
//! Display IPC operations (windows, capabilities, endpoint metadata).
2+
//! Display IPC operations (windows, capabilities, presentation, input, endpoint metadata).
3+
4+
use base64::Engine;
35

46
use super::DisplayClient;
57
use crate::DisplayError;
8+
use crate::input::InputEvent;
69
use crate::ipc::types::DisplayCapabilitiesInfo;
710
use crate::window::{CreateWindowRequest, WindowId, WindowInfo};
811

@@ -136,6 +139,124 @@ impl DisplayClient {
136139
}
137140
}
138141

142+
/// Present raw RGBA pixel data to a window (inline base64 mode).
143+
///
144+
/// The pixel data is base64-encoded and sent in the JSON-RPC request.
145+
/// For large framebuffers, prefer [`present_shm`](Self::present_shm).
146+
///
147+
/// # Errors
148+
///
149+
/// Returns an error if the IPC request fails or the server returns an error.
150+
pub async fn present(&mut self, window_id: WindowId, pixels: &[u8]) -> crate::Result<()> {
151+
let data_b64 = base64::engine::general_purpose::STANDARD.encode(pixels);
152+
let req = crate::ipc::types::JsonRpcRequest::new(
153+
"display.present",
154+
Some(serde_json::json!({
155+
"window_id": window_id.as_string(),
156+
"data": data_b64,
157+
})),
158+
);
159+
160+
let response = self.send_request(req).await?;
161+
162+
if let Some(error) = response.error {
163+
Err(DisplayError::IpcError(format!(
164+
"Server error: {}",
165+
error.message
166+
)))
167+
} else {
168+
Ok(())
169+
}
170+
}
171+
172+
/// Present framebuffer via shared memory path (zero-copy mode).
173+
///
174+
/// The client writes raw RGBA pixel data to the given path (e.g.
175+
/// `/dev/shm/toadstool-fb-{window_id}`), then calls this method.
176+
/// The server reads the file and copies it to the DRM framebuffer.
177+
///
178+
/// # Errors
179+
///
180+
/// Returns an error if the IPC request fails or the server returns an error.
181+
pub async fn present_shm(&mut self, window_id: WindowId, shm_path: &str) -> crate::Result<()> {
182+
let req = crate::ipc::types::JsonRpcRequest::new(
183+
"display.present",
184+
Some(serde_json::json!({
185+
"window_id": window_id.as_string(),
186+
"shm_path": shm_path,
187+
})),
188+
);
189+
190+
let response = self.send_request(req).await?;
191+
192+
if let Some(error) = response.error {
193+
Err(DisplayError::IpcError(format!(
194+
"Server error: {}",
195+
error.message
196+
)))
197+
} else {
198+
Ok(())
199+
}
200+
}
201+
202+
/// Subscribe to input events for a window.
203+
///
204+
/// Sets the server-side input focus to the given window so subsequent
205+
/// [`poll_events`](Self::poll_events) calls return events for it.
206+
///
207+
/// # Errors
208+
///
209+
/// Returns an error if the IPC request fails or the server returns an error.
210+
pub async fn subscribe_input(&mut self, window_id: WindowId) -> crate::Result<()> {
211+
let req = crate::ipc::types::JsonRpcRequest::new(
212+
"display.subscribe_input",
213+
Some(serde_json::json!({"window_id": window_id.as_string()})),
214+
);
215+
216+
let response = self.send_request(req).await?;
217+
218+
if let Some(error) = response.error {
219+
Err(DisplayError::IpcError(format!(
220+
"Server error: {}",
221+
error.message
222+
)))
223+
} else {
224+
Ok(())
225+
}
226+
}
227+
228+
/// Poll for pending input events (non-blocking).
229+
///
230+
/// Returns all events buffered since the last poll. Returns an empty
231+
/// `Vec` when no events are queued.
232+
///
233+
/// # Errors
234+
///
235+
/// Returns an error if the IPC request fails or the server returns an error.
236+
pub async fn poll_events(&mut self) -> crate::Result<Vec<InputEvent>> {
237+
let req = crate::ipc::types::JsonRpcRequest::new("display.poll_events", None);
238+
239+
let response = self.send_request(req).await?;
240+
241+
if let Some(result) = response.result {
242+
let events: Vec<InputEvent> = serde_json::from_value(
243+
result
244+
.get("events")
245+
.cloned()
246+
.unwrap_or(serde_json::json!([])),
247+
)
248+
.map_err(|e| DisplayError::IpcError(format!("Parse error: {e}")))?;
249+
Ok(events)
250+
} else if let Some(error) = response.error {
251+
Err(DisplayError::IpcError(format!(
252+
"Server error: {}",
253+
error.message
254+
)))
255+
} else {
256+
Err(DisplayError::IpcError("Invalid response".to_string()))
257+
}
258+
}
259+
139260
/// Get endpoint string for display purposes
140261
///
141262
/// **Helper for health checks and monitoring**

0 commit comments

Comments
 (0)