mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 14:50:03 -04:00
egui_inspection: PNG-encode screenshots on the wire; collapse protocol feature
- FrameScreenshot now carries PNG bytes instead of raw RGBA (PROTOCOL_VERSION 1→2); add a shared `encode_png` helper behind a new `png` feature so the live plugin and the kittest harness encode frames identically. - Make the protocol module unconditional: drop the `protocol` feature flag and the optional serde/serde_bytes/rmp-serde deps it gated. - plugin.rs: re-stamp screenshot-bearing frames with the current step (so inspectors waiting for step > prev don't reject them) and pump a tail-side repaint while awaiting the GPU readback.
This commit is contained in:
@@ -21,27 +21,27 @@ all-features = true
|
|||||||
rustdoc-args = ["--generate-link-to-definition"]
|
rustdoc-args = ["--generate-link-to-definition"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["protocol"]
|
default = []
|
||||||
|
|
||||||
## Wire-protocol types (`HarnessMessage`, `InspectorCommand`, …) plus length-prefixed
|
|
||||||
## MessagePack framing helpers. No `egui` dependency beyond `egui::accesskit`.
|
|
||||||
protocol = ["dep:rmp-serde", "dep:serde", "dep:serde_bytes", "egui/serde"]
|
|
||||||
|
|
||||||
## Cross-platform local-socket name helpers ([`transport::socket_name`],
|
## Cross-platform local-socket name helpers ([`transport::socket_name`],
|
||||||
## [`transport::generate_socket_target`]) built on `interprocess`: unix domain sockets on
|
## [`transport::generate_socket_target`]) built on `interprocess`: unix domain sockets on
|
||||||
## unix, named pipes on Windows. Shared by both ends of the connection.
|
## unix, named pipes on Windows. Shared by both ends of the connection.
|
||||||
transport = ["dep:interprocess", "dep:tempfile"]
|
transport = ["dep:interprocess", "dep:tempfile"]
|
||||||
|
|
||||||
|
## [`encode_png`] — shared screenshot PNG encoder so every peer (live plugin + kittest
|
||||||
|
## harness) produces identically-encoded frames.
|
||||||
|
png = ["dep:image", "image/png"]
|
||||||
|
|
||||||
## `InspectionPlugin` — an `egui::Plugin` impl that streams frames + accesskit tree to
|
## `InspectionPlugin` — an `egui::Plugin` impl that streams frames + accesskit tree to
|
||||||
## an inspector over a local socket and applies received commands. Auto-attaches when
|
## an inspector over a local socket and applies received commands. Auto-attaches when
|
||||||
## the [`INSPECTION_SOCKET_ENV_VAR`] env var is set.
|
## the [`INSPECTION_SOCKET_ENV_VAR`] env var is set.
|
||||||
plugin = ["protocol", "transport", "dep:image"]
|
plugin = ["transport", "png"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui.workspace = true
|
egui = { workspace = true, features = ["serde"] }
|
||||||
serde = { workspace = true, optional = true }
|
serde.workspace = true
|
||||||
serde_bytes = { version = "0.11.17", optional = true }
|
serde_bytes = "0.11.17"
|
||||||
rmp-serde = { workspace = true, optional = true }
|
rmp-serde.workspace = true
|
||||||
image = { workspace = true, optional = true }
|
image = { workspace = true, optional = true }
|
||||||
interprocess = { version = "2.4", optional = true }
|
interprocess = { version = "2.4", optional = true }
|
||||||
tempfile = { workspace = true, optional = true }
|
tempfile = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -3,10 +3,8 @@
|
|||||||
//! ## Feature flags
|
//! ## Feature flags
|
||||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||||
|
|
||||||
#[cfg(feature = "protocol")]
|
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
|
|
||||||
#[cfg(feature = "protocol")]
|
|
||||||
pub use protocol::{
|
pub use protocol::{
|
||||||
Capabilities, Frame, FrameScreenshot, HarnessMessage, InspectorCommand, MAX_MESSAGE_BYTES,
|
Capabilities, Frame, FrameScreenshot, HarnessMessage, InspectorCommand, MAX_MESSAGE_BYTES,
|
||||||
PROTOCOL_VERSION, PeerHello, PeerKind, SourceView, read_message, write_message,
|
PROTOCOL_VERSION, PeerHello, PeerKind, SourceView, read_message, write_message,
|
||||||
@@ -24,6 +22,12 @@ pub const INSPECTION_SOCKET_ENV_VAR: &str = "EGUI_INSPECTION_SOCKET";
|
|||||||
#[cfg(feature = "transport")]
|
#[cfg(feature = "transport")]
|
||||||
pub mod transport;
|
pub mod transport;
|
||||||
|
|
||||||
|
#[cfg(feature = "png")]
|
||||||
|
mod png;
|
||||||
|
|
||||||
|
#[cfg(feature = "png")]
|
||||||
|
pub use png::encode_png;
|
||||||
|
|
||||||
#[cfg(feature = "plugin")]
|
#[cfg(feature = "plugin")]
|
||||||
mod plugin;
|
mod plugin;
|
||||||
|
|
||||||
|
|||||||
@@ -211,11 +211,26 @@ impl egui::Plugin for InspectionPlugin {
|
|||||||
if let Some(mut frame) = self.pending_frame.take() {
|
if let Some(mut frame) = self.pending_frame.take() {
|
||||||
let [w, h] = [image.size[0] as u32, image.size[1] as u32];
|
let [w, h] = [image.size[0] as u32, image.size[1] as u32];
|
||||||
let rgba: Vec<u8> = image.pixels.iter().flat_map(|c| c.to_array()).collect();
|
let rgba: Vec<u8> = image.pixels.iter().flat_map(|c| c.to_array()).collect();
|
||||||
frame.screenshot = Some(FrameScreenshot {
|
match crate::encode_png(w, h, &rgba) {
|
||||||
width: w,
|
Ok(png) => {
|
||||||
height: h,
|
frame.screenshot = Some(FrameScreenshot {
|
||||||
rgba,
|
width: w,
|
||||||
});
|
height: h,
|
||||||
|
png,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("[INSP] PNG encode failed: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Re-stamp the frame with the *current* step. The stashed `step` was
|
||||||
|
// captured when we dispatched the screenshot command; in the meantime
|
||||||
|
// intervening frames (without screenshot) may have been emitted with
|
||||||
|
// higher step numbers. Inspectors that wait for `step > prev_step` would
|
||||||
|
// otherwise reject the screenshot-bearing frame because its step has
|
||||||
|
// regressed.
|
||||||
|
self.step = self.step.saturating_add(1);
|
||||||
|
frame.step = self.step;
|
||||||
self.send(HarnessMessage::Frame(Box::new(frame)));
|
self.send(HarnessMessage::Frame(Box::new(frame)));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -296,6 +311,17 @@ impl egui::Plugin for InspectionPlugin {
|
|||||||
if !want_screenshot {
|
if !want_screenshot {
|
||||||
// No screenshot needed — emit immediately.
|
// No screenshot needed — emit immediately.
|
||||||
self.send(HarnessMessage::Frame(Box::new(frame)));
|
self.send(HarnessMessage::Frame(Box::new(frame)));
|
||||||
|
// If we're still waiting on a screenshot from a previous dispatch, keep
|
||||||
|
// pumping repaints from the end of the frame too. `input_hook` already
|
||||||
|
// does this at frame start, but on reactive apps the GPU readback can
|
||||||
|
// take several frames to fulfill — without a tail-side repaint the
|
||||||
|
// integration may go idle between `input_hook` ticks once the captured
|
||||||
|
// frame finishes presenting.
|
||||||
|
if self.awaiting_screenshot {
|
||||||
|
if let Some(ctx) = self.shared_ctx.get() {
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
21
crates/egui_inspection/src/png.rs
Normal file
21
crates/egui_inspection/src/png.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
//! Shared screenshot PNG encoder.
|
||||||
|
//!
|
||||||
|
//! Both peers — the live [`crate::InspectionPlugin`] and `egui_kittest`'s harness inspector
|
||||||
|
//! — encode their frames here so they produce identically-encoded
|
||||||
|
//! [`crate::protocol::FrameScreenshot`]s.
|
||||||
|
|
||||||
|
/// Encode tightly-packed RGBA8 pixels (`width * height * 4` bytes) as PNG using `image`'s
|
||||||
|
/// default settings (`CompressionType::Default` + `FilterType::Adaptive`).
|
||||||
|
///
|
||||||
|
/// PNG keeps high-resolution captures off the hot path of socket throughput — a 1550×2114
|
||||||
|
/// RGBA8 buffer is ~13 MiB raw but typically <1 MiB encoded.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// When the encoder fails (e.g. the buffer length doesn't match `width * height * 4`).
|
||||||
|
pub fn encode_png(width: u32, height: u32, rgba: &[u8]) -> Result<Vec<u8>, image::ImageError> {
|
||||||
|
use image::ImageEncoder as _;
|
||||||
|
let mut out = std::io::Cursor::new(Vec::new());
|
||||||
|
image::codecs::png::PngEncoder::new(&mut out)
|
||||||
|
.write_image(rgba, width, height, image::ExtendedColorType::Rgba8)?;
|
||||||
|
Ok(out.into_inner())
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ use egui::accesskit;
|
|||||||
/// non-additive change is made to [`HarnessMessage`] / [`InspectorCommand`] / their
|
/// non-additive change is made to [`HarnessMessage`] / [`InspectorCommand`] / their
|
||||||
/// payload structs. The inspector should refuse peers with a higher major version than
|
/// payload structs. The inspector should refuse peers with a higher major version than
|
||||||
/// it understands.
|
/// it understands.
|
||||||
pub const PROTOCOL_VERSION: u32 = 1;
|
pub const PROTOCOL_VERSION: u32 = 2;
|
||||||
|
|
||||||
/// What kind of egui peer the inspector is talking to. Determines which controls the
|
/// What kind of egui peer the inspector is talking to. Determines which controls the
|
||||||
/// inspector UI should render (Step / Pause buttons make no sense against a live app).
|
/// inspector UI should render (Step / Pause buttons make no sense against a live app).
|
||||||
@@ -132,11 +132,13 @@ pub struct FrameScreenshot {
|
|||||||
pub width: u32,
|
pub width: u32,
|
||||||
/// Image height in physical pixels.
|
/// Image height in physical pixels.
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
/// Tightly packed RGBA8 pixels (length = `width * height * 4`). `serde_bytes` encodes
|
/// PNG-encoded image bytes. PNG compression keeps high-resolution captures off the
|
||||||
/// this as a msgpack `bin` blob (one type tag + raw bytes) instead of the default
|
/// hot path of unix-socket throughput — a 1550×2114 RGBA8 buffer is ~13 MiB raw but
|
||||||
/// `Vec<u8>` path of one type tag *per byte*, which would roughly double on-wire size.
|
/// typically <1 MiB as PNG. `serde_bytes` encodes this as a msgpack `bin` blob (one
|
||||||
|
/// type tag + raw bytes) instead of the default `Vec<u8>` path of one type tag *per
|
||||||
|
/// byte*, which would roughly double on-wire size.
|
||||||
#[serde(with = "serde_bytes")]
|
#[serde(with = "serde_bytes")]
|
||||||
pub rgba: Vec<u8>,
|
pub png: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A single update from the egui peer: accesskit tree + optional screenshot.
|
/// A single update from the egui peer: accesskit tree + optional screenshot.
|
||||||
|
|||||||
Reference in New Issue
Block a user