summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Bingus <shankern@protonmail.com>2023-03-25 10:45:39 -0700
committerLibravatar Héctor Ramón Jiménez <hector0193@gmail.com>2023-06-06 15:37:30 +0200
commit233196eb14b40f8bd5201ea0262571f82136ad53 (patch)
tree410cf71cda98bdcb55ecc384f7fbc2f4ef669edd
parentc15f1b5f6575792cc89bb5fba2e613428397e46a (diff)
downloadiced-233196eb14b40f8bd5201ea0262571f82136ad53.tar.gz
iced-233196eb14b40f8bd5201ea0262571f82136ad53.tar.bz2
iced-233196eb14b40f8bd5201ea0262571f82136ad53.zip
Added offscreen rendering support for wgpu & tiny-skia exposed with the window::screenshot command.
-rw-r--r--examples/screenshot/Cargo.toml11
-rw-r--r--examples/screenshot/src/main.rs305
-rw-r--r--graphics/src/compositor.rs15
-rw-r--r--renderer/src/compositor.rs30
-rw-r--r--runtime/src/lib.rs2
-rw-r--r--runtime/src/screenshot.rs80
-rw-r--r--runtime/src/window.rs8
-rw-r--r--runtime/src/window/action.rs9
-rw-r--r--tiny_skia/src/window/compositor.rs76
-rw-r--r--wgpu/src/backend.rs61
-rw-r--r--wgpu/src/lib.rs1
-rw-r--r--wgpu/src/offscreen.rs102
-rw-r--r--wgpu/src/shader/offscreen_blit.wgsl22
-rw-r--r--wgpu/src/triangle/msaa.rs11
-rw-r--r--wgpu/src/window/compositor.rs143
-rw-r--r--winit/src/application.rs41
16 files changed, 893 insertions, 24 deletions
diff --git a/examples/screenshot/Cargo.toml b/examples/screenshot/Cargo.toml
new file mode 100644
index 00000000..b79300b7
--- /dev/null
+++ b/examples/screenshot/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "screenshot"
+version = "0.1.0"
+authors = ["Bingus <shankern@protonmail.com>"]
+edition = "2021"
+publish = false
+
+[dependencies]
+iced = { path = "../..", features = ["debug", "image", "advanced"] }
+image = { version = "0.24.6", features = ["png"]}
+env_logger = "0.10.0"
diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs
new file mode 100644
index 00000000..29d961d9
--- /dev/null
+++ b/examples/screenshot/src/main.rs
@@ -0,0 +1,305 @@
+use iced::alignment::{Horizontal, Vertical};
+use iced::keyboard::KeyCode;
+use iced::theme::{Button, Container};
+use iced::widget::runtime::{CropError, Screenshot};
+use iced::widget::{
+ button, column as col, container, image as iced_image, row, text,
+ text_input,
+};
+use iced::{
+ event, executor, keyboard, subscription, Alignment, Application, Command,
+ ContentFit, Element, Event, Length, Rectangle, Renderer, Subscription,
+ Theme,
+};
+use image as img;
+use image::ColorType;
+
+fn main() -> iced::Result {
+ env_logger::builder().format_timestamp(None).init();
+
+ Example::run(iced::Settings::default())
+}
+
+struct Example {
+ screenshot: Option<Screenshot>,
+ saved_png_path: Option<Result<String, PngError>>,
+ png_saving: bool,
+ crop_error: Option<CropError>,
+ x_input_value: u32,
+ y_input_value: u32,
+ width_input_value: u32,
+ height_input_value: u32,
+}
+
+#[derive(Clone, Debug)]
+enum Message {
+ Crop,
+ Screenshot,
+ ScreenshotData(Screenshot),
+ Png,
+ PngSaved(Result<String, PngError>),
+ XInputChanged(String),
+ YInputChanged(String),
+ WidthInputChanged(String),
+ HeightInputChanged(String),
+}
+
+impl Application for Example {
+ type Executor = executor::Default;
+ type Message = Message;
+ type Theme = Theme;
+ type Flags = ();
+
+ fn new(_flags: Self::Flags) -> (Self, Command<Self::Message>) {
+ (
+ Example {
+ screenshot: None,
+ saved_png_path: None,
+ png_saving: false,
+ crop_error: None,
+ x_input_value: 0,
+ y_input_value: 0,
+ width_input_value: 0,
+ height_input_value: 0,
+ },
+ Command::none(),
+ )
+ }
+
+ fn title(&self) -> String {
+ "Screenshot".to_string()
+ }
+
+ fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
+ match message {
+ Message::Screenshot => {
+ return iced::window::screenshot(Message::ScreenshotData);
+ }
+ Message::ScreenshotData(screenshot) => {
+ self.screenshot = Some(screenshot);
+ }
+ Message::Png => {
+ if let Some(screenshot) = &self.screenshot {
+ return Command::perform(
+ save_to_png(screenshot.clone()),
+ Message::PngSaved,
+ );
+ }
+ self.png_saving = true;
+ }
+ Message::PngSaved(res) => {
+ self.png_saving = false;
+ self.saved_png_path = Some(res);
+ }
+ Message::XInputChanged(new) => {
+ if let Ok(value) = new.parse::<u32>() {
+ self.x_input_value = value;
+ }
+ }
+ Message::YInputChanged(new) => {
+ if let Ok(value) = new.parse::<u32>() {
+ self.y_input_value = value;
+ }
+ }
+ Message::WidthInputChanged(new) => {
+ if let Ok(value) = new.parse::<u32>() {
+ self.width_input_value = value;
+ }
+ }
+ Message::HeightInputChanged(new) => {
+ if let Ok(value) = new.parse::<u32>() {
+ self.height_input_value = value;
+ }
+ }
+ Message::Crop => {
+ if let Some(screenshot) = &self.screenshot {
+ let cropped = screenshot.crop(Rectangle::<u32> {
+ x: self.x_input_value,
+ y: self.y_input_value,
+ width: self.width_input_value,
+ height: self.height_input_value,
+ });
+
+ match cropped {
+ Ok(screenshot) => {
+ self.screenshot = Some(screenshot);
+ self.crop_error = None;
+ }
+ Err(crop_error) => {
+ self.crop_error = Some(crop_error);
+ }
+ }
+ }
+ }
+ }
+
+ Command::none()
+ }
+
+ fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> {
+ let image: Element<Message> = if let Some(screenshot) = &self.screenshot
+ {
+ iced_image(iced_image::Handle::from_pixels(
+ screenshot.size.width,
+ screenshot.size.height,
+ screenshot.bytes.clone(),
+ ))
+ .content_fit(ContentFit::ScaleDown)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .into()
+ } else {
+ text("Press the button to take a screenshot!").into()
+ };
+
+ let image = container(image)
+ .padding(10)
+ .style(Container::Custom(Box::new(ScreenshotDisplayContainer)))
+ .width(Length::FillPortion(2))
+ .height(Length::Fill)
+ .center_x()
+ .center_y();
+
+ let crop_origin_controls = row![
+ text("X:").vertical_alignment(Vertical::Center).width(14),
+ text_input("0", &format!("{}", self.x_input_value),)
+ .on_input(Message::XInputChanged)
+ .width(40),
+ text("Y:").vertical_alignment(Vertical::Center).width(14),
+ text_input("0", &format!("{}", self.y_input_value),)
+ .on_input(Message::YInputChanged)
+ .width(40),
+ ]
+ .spacing(10)
+ .align_items(Alignment::Center);
+
+ let crop_dimension_controls = row![
+ text("W:").vertical_alignment(Vertical::Center).width(14),
+ text_input("0", &format!("{}", self.width_input_value),)
+ .on_input(Message::WidthInputChanged)
+ .width(40),
+ text("H:").vertical_alignment(Vertical::Center).width(14),
+ text_input("0", &format!("{}", self.height_input_value),)
+ .on_input(Message::HeightInputChanged)
+ .width(40),
+ ]
+ .spacing(10)
+ .align_items(Alignment::Center);
+
+ let mut crop_controls =
+ col![crop_origin_controls, crop_dimension_controls]
+ .spacing(10)
+ .align_items(Alignment::Center);
+
+ if let Some(crop_error) = &self.crop_error {
+ crop_controls = crop_controls
+ .push(text(format!("Crop error! \n{}", crop_error)));
+ }
+
+ let png_button = if !self.png_saving {
+ button("Save to png.")
+ .style(Button::Secondary)
+ .padding([10, 20, 10, 20])
+ .on_press(Message::Png)
+ } else {
+ button("Saving..")
+ .style(Button::Secondary)
+ .padding([10, 20, 10, 20])
+ };
+
+ let mut controls = col![
+ button("Screenshot!")
+ .padding([10, 20, 10, 20])
+ .on_press(Message::Screenshot),
+ button("Crop")
+ .style(Button::Destructive)
+ .padding([10, 20, 10, 20])
+ .on_press(Message::Crop),
+ crop_controls,
+ png_button,
+ ]
+ .spacing(40)
+ .align_items(Alignment::Center);
+
+ if let Some(png_result) = &self.saved_png_path {
+ let msg = match png_result {
+ Ok(path) => format!("Png saved as: {:?}!", path),
+ Err(msg) => {
+ format!("Png could not be saved due to:\n{:?}", msg)
+ }
+ };
+
+ controls = controls.push(text(msg));
+ }
+
+ let side_content = container(controls)
+ .align_x(Horizontal::Center)
+ .width(Length::FillPortion(1))
+ .height(Length::Fill)
+ .center_y()
+ .center_x();
+
+ let content = row![side_content, image]
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .align_items(Alignment::Center);
+
+ container(content)
+ .padding(10)
+ .width(Length::Fill)
+ .height(Length::Fill)
+ .center_x()
+ .center_y()
+ .into()
+ }
+
+ fn subscription(&self) -> Subscription<Self::Message> {
+ subscription::events_with(|event, status| {
+ if let event::Status::Captured = status {
+ return None;
+ }
+
+ if let Event::Keyboard(keyboard::Event::KeyPressed {
+ key_code: KeyCode::F5,
+ ..
+ }) = event
+ {
+ Some(Message::Screenshot)
+ } else {
+ None
+ }
+ })
+ }
+}
+
+struct ScreenshotDisplayContainer;
+
+impl container::StyleSheet for ScreenshotDisplayContainer {
+ type Style = Theme;
+
+ fn appearance(&self, style: &Self::Style) -> container::Appearance {
+ container::Appearance {
+ text_color: None,
+ background: None,
+ border_radius: 5.0,
+ border_width: 4.0,
+ border_color: style.palette().primary,
+ }
+ }
+}
+
+async fn save_to_png(screenshot: Screenshot) -> Result<String, PngError> {
+ let path = "screenshot.png".to_string();
+ img::save_buffer(
+ &path,
+ &screenshot.bytes,
+ screenshot.size.width,
+ screenshot.size.height,
+ ColorType::Rgba8,
+ )
+ .map(|_| path)
+ .map_err(|err| PngError(format!("{:?}", err)))
+}
+
+#[derive(Clone, Debug)]
+struct PngError(String);
diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs
index d55e801a..f7b86045 100644
--- a/graphics/src/compositor.rs
+++ b/graphics/src/compositor.rs
@@ -59,6 +59,19 @@ pub trait Compositor: Sized {
background_color: Color,
overlay: &[T],
) -> Result<(), SurfaceError>;
+
+ /// Screenshots the current [`Renderer`] primitives to an offscreen texture, and returns the bytes of
+ /// the texture ordered as `RGBA` in the sRGB color space.
+ ///
+ /// [`Renderer`]: Self::Renderer;
+ fn screenshot<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ surface: &mut Self::Surface,
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Vec<u8>;
}
/// Result of an unsuccessful call to [`Compositor::present`].
@@ -82,7 +95,7 @@ pub enum SurfaceError {
OutOfMemory,
}
-/// Contains informations about the graphics (e.g. graphics adapter, graphics backend).
+/// Contains information about the graphics (e.g. graphics adapter, graphics backend).
#[derive(Debug)]
pub struct Information {
/// Contains the graphics adapter.
diff --git a/renderer/src/compositor.rs b/renderer/src/compositor.rs
index a353b8e4..57317b28 100644
--- a/renderer/src/compositor.rs
+++ b/renderer/src/compositor.rs
@@ -136,6 +136,36 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
}
})
}
+
+ fn screenshot<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ surface: &mut Self::Surface,
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Vec<u8> {
+ renderer.with_primitives(|backend, primitives| match (self, backend, surface) {
+ (Self::TinySkia(_compositor), crate::Backend::TinySkia(backend), Surface::TinySkia(surface)) => {
+ iced_tiny_skia::window::compositor::screenshot(surface, backend, primitives, viewport, background_color, overlay)
+ },
+ #[cfg(feature = "wgpu")]
+ (Self::Wgpu(compositor), crate::Backend::Wgpu(backend), Surface::Wgpu(_)) => {
+ iced_wgpu::window::compositor::screenshot(
+ compositor,
+ backend,
+ primitives,
+ viewport,
+ background_color,
+ overlay,
+ )
+ },
+ #[allow(unreachable_patterns)]
+ _ => panic!(
+ "The provided renderer or backend are not compatible with the compositor."
+ ),
+ })
+ }
}
enum Candidate {
diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs
index 50abf7b2..32ed14d8 100644
--- a/runtime/src/lib.rs
+++ b/runtime/src/lib.rs
@@ -60,6 +60,7 @@ mod debug;
#[cfg(not(feature = "debug"))]
#[path = "debug/null.rs"]
mod debug;
+mod screenshot;
pub use iced_core as core;
pub use iced_futures as futures;
@@ -68,4 +69,5 @@ pub use command::Command;
pub use debug::Debug;
pub use font::Font;
pub use program::Program;
+pub use screenshot::{CropError, Screenshot};
pub use user_interface::UserInterface;
diff --git a/runtime/src/screenshot.rs b/runtime/src/screenshot.rs
new file mode 100644
index 00000000..527e400f
--- /dev/null
+++ b/runtime/src/screenshot.rs
@@ -0,0 +1,80 @@
+use iced_core::{Rectangle, Size};
+use std::fmt::{Debug, Formatter};
+
+/// Data of a screenshot, captured with `window::screenshot()`.
+///
+/// The `bytes` of this screenshot will always be ordered as `RGBA` in the sRGB color space.
+#[derive(Clone)]
+pub struct Screenshot {
+ /// The bytes of the [`Screenshot`].
+ pub bytes: Vec<u8>,
+ /// The size of the [`Screenshot`].
+ pub size: Size<u32>,
+}
+
+impl Debug for Screenshot {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "Screenshot: {{ \n bytes: {}\n size: {:?} }}",
+ self.bytes.len(),
+ self.size
+ )
+ }
+}
+
+impl Screenshot {
+ /// Creates a new [`Screenshot`].
+ pub fn new(bytes: Vec<u8>, size: Size<u32>) -> Self {
+ Self { bytes, size }
+ }
+
+ /// Crops a [`Screenshot`] to the provided `region`. This will always be relative to the
+ /// top-left corner of the [`Screenshot`].
+ pub fn crop(&self, region: Rectangle<u32>) -> Result<Self, CropError> {
+ if region.width == 0 || region.height == 0 {
+ return Err(CropError::Zero);
+ }
+
+ if region.x + region.width > self.size.width
+ || region.y + region.height > self.size.height
+ {
+ return Err(CropError::OutOfBounds);
+ }
+
+ // Image is always RGBA8 = 4 bytes per pixel
+ const PIXEL_SIZE: usize = 4;
+
+ let bytes_per_row = self.size.width as usize * PIXEL_SIZE;
+ let row_range = region.y as usize..(region.y + region.height) as usize;
+ let column_range = region.x as usize * PIXEL_SIZE
+ ..(region.x + region.width) as usize * PIXEL_SIZE;
+
+ let chopped = self.bytes.chunks(bytes_per_row).enumerate().fold(
+ vec![],
+ |mut acc, (row, bytes)| {
+ if row_range.contains(&row) {
+ acc.extend(&bytes[column_range.clone()]);
+ }
+
+ acc
+ },
+ );
+
+ Ok(Self {
+ bytes: chopped,
+ size: Size::new(region.width, region.height),
+ })
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+/// Errors that can occur when cropping a [`Screenshot`].
+pub enum CropError {
+ #[error("The cropped region is out of bounds.")]
+ /// The cropped region's size is out of bounds.
+ OutOfBounds,
+ #[error("The cropped region is not visible.")]
+ /// The cropped region's size is zero.
+ Zero,
+}
diff --git a/runtime/src/window.rs b/runtime/src/window.rs
index d4111293..9b66cb0e 100644
--- a/runtime/src/window.rs
+++ b/runtime/src/window.rs
@@ -7,6 +7,7 @@ use crate::command::{self, Command};
use crate::core::time::Instant;
use crate::core::window::{Event, Icon, Level, Mode, UserAttention};
use crate::futures::subscription::{self, Subscription};
+use crate::screenshot::Screenshot;
/// Subscribes to the frames of the window of the running application.
///
@@ -115,3 +116,10 @@ pub fn fetch_id<Message>(
pub fn change_icon<Message>(icon: Icon) -> Command<Message> {
Command::single(command::Action::Window(Action::ChangeIcon(icon)))
}
+
+/// Captures a [`Screenshot`] from the window.
+pub fn screenshot<Message>(
+ f: impl FnOnce(Screenshot) -> Message + Send + 'static,
+) -> Command<Message> {
+ Command::single(command::Action::Window(Action::Screenshot(Box::new(f))))
+}
diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs
index a9d2a3d0..cb430681 100644
--- a/runtime/src/window/action.rs
+++ b/runtime/src/window/action.rs
@@ -1,6 +1,7 @@
use crate::core::window::{Icon, Level, Mode, UserAttention};
use crate::futures::MaybeSend;
+use crate::screenshot::Screenshot;
use std::fmt;
/// An operation to be performed on some window.
@@ -89,6 +90,8 @@ pub enum Action<T> {
/// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That
/// said, it's usually in the same ballpark as on Windows.
ChangeIcon(Icon),
+ /// Screenshot the viewport of the window.
+ Screenshot(Box<dyn FnOnce(Screenshot) -> T + 'static>),
}
impl<T> Action<T> {
@@ -118,6 +121,11 @@ impl<T> Action<T> {
Self::ChangeLevel(level) => Action::ChangeLevel(level),
Self::FetchId(o) => Action::FetchId(Box::new(move |s| f(o(s)))),
Self::ChangeIcon(icon) => Action::ChangeIcon(icon),
+ Self::Screenshot(tag) => {
+ Action::Screenshot(Box::new(move |screenshot| {
+ f(tag(screenshot))
+ }))
+ }
}
}
}
@@ -155,6 +163,7 @@ impl<T> fmt::Debug for Action<T> {
Self::ChangeIcon(_icon) => {
write!(f, "Action::ChangeIcon(icon)")
}
+ Self::Screenshot(_) => write!(f, "Action::Screenshot"),
}
}
}
diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs
index 9999a188..f3be3f16 100644
--- a/tiny_skia/src/window/compositor.rs
+++ b/tiny_skia/src/window/compositor.rs
@@ -1,5 +1,5 @@
-use crate::core::{Color, Rectangle};
-use crate::graphics::compositor::{self, Information, SurfaceError};
+use crate::core::{Color, Rectangle, Size};
+use crate::graphics::compositor::{self, Information};
use crate::graphics::damage;
use crate::graphics::{Error, Primitive, Viewport};
use crate::{Backend, Renderer, Settings};
@@ -79,7 +79,7 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
viewport: &Viewport,
background_color: Color,
overlay: &[T],
- ) -> Result<(), SurfaceError> {
+ ) -> Result<(), compositor::SurfaceError> {
renderer.with_primitives(|backend, primitives| {
present(
backend,
@@ -91,6 +91,26 @@ impl<Theme> crate::graphics::Compositor for Compositor<Theme> {
)
})
}
+
+ fn screenshot<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ surface: &mut Self::Surface,
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Vec<u8> {
+ renderer.with_primitives(|backend, primitives| {
+ screenshot(
+ surface,
+ backend,
+ primitives,
+ viewport,
+ background_color,
+ overlay,
+ )
+ })
+ }
}
pub fn new<Theme>(settings: Settings) -> (Compositor<Theme>, Backend) {
@@ -156,3 +176,53 @@ pub fn present<T: AsRef<str>>(
Ok(())
}
+
+pub fn screenshot<T: AsRef<str>>(
+ surface: &mut Surface,
+ backend: &mut Backend,
+ primitives: &[Primitive],
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+) -> Vec<u8> {
+ let size = viewport.physical_size();
+
+ let mut offscreen_buffer: Vec<u32> =
+ vec![0; size.width as usize * size.height as usize];
+
+ backend.draw(
+ &mut tiny_skia::PixmapMut::from_bytes(
+ bytemuck::cast_slice_mut(&mut offscreen_buffer),
+ size.width,
+ size.height,
+ )
+ .expect("Create offscreen pixel map"),
+ &mut surface.clip_mask,
+ primitives,
+ viewport,
+ &[Rectangle::with_size(Size::new(
+ size.width as f32,
+ size.height as f32,
+ ))],
+ background_color,
+ overlay,
+ );
+
+ offscreen_buffer.iter().fold(
+ Vec::with_capacity(offscreen_buffer.len() * 4),
+ |mut acc, pixel| {
+ const A_MASK: u32 = 0xFF_00_00_00;
+ const R_MASK: u32 = 0x00_FF_00_00;
+ const G_MASK: u32 = 0x00_00_FF_00;
+ const B_MASK: u32 = 0x00_00_00_FF;
+
+ let a = ((A_MASK & pixel) >> 24) as u8;
+ let r = ((R_MASK & pixel) >> 16) as u8;
+ let g = ((G_MASK & pixel) >> 8) as u8;
+ let b = (B_MASK & pixel) as u8;
+
+ acc.extend([r, g, b, a]);
+ acc
+ },
+ )
+}
diff --git a/wgpu/src/backend.rs b/wgpu/src/backend.rs
index b524c615..8f37f285 100644
--- a/wgpu/src/backend.rs
+++ b/wgpu/src/backend.rs
@@ -1,4 +1,3 @@
-use crate::core;
use crate::core::{Color, Font, Point, Size};
use crate::graphics::backend;
use crate::graphics::color;
@@ -6,6 +5,7 @@ use crate::graphics::{Primitive, Transformation, Viewport};
use crate::quad;
use crate::text;
use crate::triangle;
+use crate::{core, offscreen};
use crate::{Layer, Settings};
#[cfg(feature = "tracing")]
@@ -123,6 +123,65 @@ impl Backend {
self.image_pipeline.end_frame();
}
+ /// Performs an offscreen render pass. If the `format` selected by WGPU is not
+ /// `wgpu::TextureFormat::Rgba8UnormSrgb`, a conversion compute pipeline will run.
+ ///
+ /// Returns `None` if the `frame` is `Rgba8UnormSrgb`, else returns the newly
+ /// converted texture view in `Rgba8UnormSrgb`.
+ pub fn offscreen<T: AsRef<str>>(
+ &mut self,
+ device: &wgpu::Device,
+ queue: &wgpu::Queue,
+ encoder: &mut wgpu::CommandEncoder,
+ clear_color: Option<Color>,
+ frame: &wgpu::TextureView,
+ format: wgpu::TextureFormat,
+ primitives: &[Primitive],
+ viewport: &Viewport,
+ overlay_text: &[T],
+ texture_extent: wgpu::Extent3d,
+ ) -> Option<wgpu::Texture> {
+ #[cfg(feature = "tracing")]
+ let _ = info_span!("iced_wgpu::offscreen", "DRAW").entered();
+
+ self.present(
+ device,
+ queue,
+ encoder,
+ clear_color,
+ frame,
+ primitives,
+ viewport,
+ overlay_text,
+ );
+
+ if format != wgpu::TextureFormat::Rgba8UnormSrgb {
+ log::info!("Texture format is {format:?}; performing conversion to rgba8..");
+ let pipeline = offscreen::Pipeline::new(device);
+
+ let texture = device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("iced_wgpu.offscreen.conversion.source_texture"),
+ size: texture_extent,
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: wgpu::TextureFormat::Rgba8Unorm,
+ usage: wgpu::TextureUsages::STORAGE_BINDING
+ | wgpu::TextureUsages::COPY_SRC,
+ view_formats: &[],
+ });
+
+ let view =
+ texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ pipeline.convert(device, texture_extent, frame, &view, encoder);
+
+ return Some(texture);
+ }
+
+ None
+ }
+
fn prepare_text(
&mut self,
device: &wgpu::Device,
diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs
index 0a5726b5..827acb89 100644
--- a/wgpu/src/lib.rs
+++ b/wgpu/src/lib.rs
@@ -46,6 +46,7 @@ pub mod geometry;
mod backend;
mod buffer;
+mod offscreen;
mod quad;
mod text;
mod triangle;
diff --git a/wgpu/src/offscreen.rs b/wgpu/src/offscreen.rs
new file mode 100644
index 00000000..29913d02
--- /dev/null
+++ b/wgpu/src/offscreen.rs
@@ -0,0 +1,102 @@
+use std::borrow::Cow;
+
+/// A simple compute pipeline to convert any texture to Rgba8UnormSrgb.
+#[derive(Debug)]
+pub struct Pipeline {
+ pipeline: wgpu::ComputePipeline,
+ layout: wgpu::BindGroupLayout,
+}
+
+impl Pipeline {
+ pub fn new(device: &wgpu::Device) -> Self {
+ let shader =
+ device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("iced_wgpu.offscreen.blit.shader"),
+ source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!(
+ "shader/offscreen_blit.wgsl"
+ ))),
+ });
+
+ let bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("iced_wgpu.offscreen.blit.bind_group_layout"),
+ entries: &[
+ wgpu::BindGroupLayoutEntry {
+ binding: 0,
+ visibility: wgpu::ShaderStages::COMPUTE,
+ ty: wgpu::BindingType::Texture {
+ sample_type: wgpu::TextureSampleType::Float {
+ filterable: false,
+ },
+ view_dimension: wgpu::TextureViewDimension::D2,
+ multisampled: false,
+ },
+ count: None,
+ },
+ wgpu::BindGroupLayoutEntry {
+ binding: 1,
+ visibility: wgpu::ShaderStages::COMPUTE,
+ ty: wgpu::BindingType::StorageTexture {
+ access: wgpu::StorageTextureAccess::WriteOnly,
+ format: wgpu::TextureFormat::Rgba8Unorm,
+ view_dimension: wgpu::TextureViewDimension::D2,
+ },
+ count: None,
+ },
+ ],
+ });
+
+ let pipeline_layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("iced_wgpu.offscreen.blit.pipeline_layout"),
+ bind_group_layouts: &[&bind_group_layout],
+ push_constant_ranges: &[],
+ });
+
+ let pipeline =
+ device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
+ label: Some("iced_wgpu.offscreen.blit.pipeline"),
+ layout: Some(&pipeline_layout),
+ module: &shader,
+ entry_point: "main",
+ });
+
+ Self {
+ pipeline,
+ layout: bind_group_layout,
+ }
+ }
+
+ pub fn convert(
+ &self,
+ device: &wgpu::Device,
+ extent: wgpu::Extent3d,
+ frame: &wgpu::TextureView,
+ view: &wgpu::TextureView,
+ encoder: &mut wgpu::CommandEncoder,
+ ) {
+ let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("iced_wgpu.offscreen.blit.bind_group"),
+ layout: &self.layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: wgpu::BindingResource::TextureView(frame),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::TextureView(view),
+ },
+ ],
+ });
+
+ let mut compute_pass =
+ encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
+ label: Some("iced_wgpu.offscreen.blit.compute_pass"),
+ });
+
+ compute_pass.set_pipeline(&self.pipeline);
+ compute_pass.set_bind_group(0, &bind, &[]);
+ compute_pass.dispatch_workgroups(extent.width, extent.height, 1);
+ }
+}
diff --git a/wgpu/src/shader/offscreen_blit.wgsl b/wgpu/src/shader/offscreen_blit.wgsl
new file mode 100644
index 00000000..9c764c36
--- /dev/null
+++ b/wgpu/src/shader/offscreen_blit.wgsl
@@ -0,0 +1,22 @@
+@group(0) @binding(0) var u_texture: texture_2d<f32>;
+@group(0) @binding(1) var out_texture: texture_storage_2d<rgba8unorm, write>;
+
+fn srgb(color: f32) -> f32 {
+ if (color <= 0.0031308) {
+ return 12.92 * color;
+ } else {
+ return (1.055 * (pow(color, (1.0/2.4)))) - 0.055;
+ }
+}
+
+@compute @workgroup_size(1)
+fn main(@builtin(global_invocation_id) id: vec3<u32>) {
+ // texture coord must be i32 due to a naga bug:
+ // https://github.com/gfx-rs/naga/issues/1997
+ let coords = vec2(i32(id.x), i32(id.y));
+
+ let src: vec4<f32> = textureLoad(u_texture, coords, 0);
+ let srgb_color: vec4<f32> = vec4(srgb(src.x), srgb(src.y), srgb(src.z), src.w);
+
+ textureStore(out_texture, coords, srgb_color);
+}
diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs
index 4afbdb32..320b5b12 100644
--- a/wgpu/src/triangle/msaa.rs
+++ b/wgpu/src/triangle/msaa.rs
@@ -16,15 +16,8 @@ impl Blit {
format: wgpu::TextureFormat,
antialiasing: graphics::Antialiasing,
) -> Blit {
- let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
- address_mode_u: wgpu::AddressMode::ClampToEdge,
- address_mode_v: wgpu::AddressMode::ClampToEdge,
- address_mode_w: wgpu::AddressMode::ClampToEdge,
- mag_filter: wgpu::FilterMode::Nearest,
- min_filter: wgpu::FilterMode::Nearest,
- mipmap_filter: wgpu::FilterMode::Nearest,
- ..Default::default()
- });
+ let sampler =
+ device.create_sampler(&wgpu::SamplerDescriptor::default());
let constant_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs
index 2eaafde0..43c3dce5 100644
--- a/wgpu/src/window/compositor.rs
+++ b/wgpu/src/window/compositor.rs
@@ -1,5 +1,5 @@
//! Connect a window with a renderer.
-use crate::core::Color;
+use crate::core::{Color, Size};
use crate::graphics;
use crate::graphics::color;
use crate::graphics::compositor;
@@ -283,4 +283,145 @@ impl<Theme> graphics::Compositor for Compositor<Theme> {
)
})
}
+
+ fn screenshot<T: AsRef<str>>(
+ &mut self,
+ renderer: &mut Self::Renderer,
+ _surface: &mut Self::Surface,
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+ ) -> Vec<u8> {
+ renderer.with_primitives(|backend, primitives| {
+ screenshot(
+ self,
+ backend,
+ primitives,
+ viewport,
+ background_color,
+ overlay,
+ )
+ })
+ }
+}
+
+/// Renders the current surface to an offscreen buffer.
+///
+/// Returns RGBA bytes of the texture data.
+pub fn screenshot<Theme, T: AsRef<str>>(
+ compositor: &Compositor<Theme>,
+ backend: &mut Backend,
+ primitives: &[Primitive],
+ viewport: &Viewport,
+ background_color: Color,
+ overlay: &[T],
+) -> Vec<u8> {
+ let mut encoder = compositor.device.create_command_encoder(
+ &wgpu::CommandEncoderDescriptor {
+ label: Some("iced_wgpu.offscreen.encoder"),
+ },
+ );
+
+ let dimensions = BufferDimensions::new(viewport.physical_size());
+
+ let texture_extent = wgpu::Extent3d {
+ width: dimensions.width,
+ height: dimensions.height,
+ depth_or_array_layers: 1,
+ };
+
+ let texture = compositor.device.create_texture(&wgpu::TextureDescriptor {
+ label: Some("iced_wgpu.offscreen.source_texture"),
+ size: texture_extent,
+ mip_level_count: 1,
+ sample_count: 1,
+ dimension: wgpu::TextureDimension::D2,
+ format: compositor.format,
+ usage: wgpu::TextureUsages::RENDER_ATTACHMENT
+ | wgpu::TextureUsages::COPY_SRC
+ | wgpu::TextureUsages::TEXTURE_BINDING,
+ view_formats: &[],
+ });
+
+ let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
+
+ let rgba_texture = backend.offscreen(
+ &compositor.device,
+ &compositor.queue,
+ &mut encoder,
+ Some(background_color),
+ &view,
+ compositor.format,
+ primitives,
+ viewport,
+ overlay,
+ texture_extent,
+ );
+
+ let output_buffer =
+ compositor.device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("iced_wgpu.offscreen.output_texture_buffer"),
+ size: (dimensions.padded_bytes_per_row * dimensions.height as usize)
+ as u64,
+ usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ encoder.copy_texture_to_buffer(
+ rgba_texture.unwrap_or(texture).as_image_copy(),
+ wgpu::ImageCopyBuffer {
+ buffer: &output_buffer,
+ layout: wgpu::ImageDataLayout {
+ offset: 0,
+ bytes_per_row: Some(dimensions.padded_bytes_per_row as u32),
+ rows_per_image: None,
+ },
+ },
+ texture_extent,
+ );
+
+ let index = compositor.queue.submit(Some(encoder.finish()));
+
+ let slice = output_buffer.slice(..);
+ slice.map_async(wgpu::MapMode::Read, |_| {});
+
+ let _ = compositor
+ .device
+ .poll(wgpu::Maintain::WaitForSubmissionIndex(index));
+
+ let mapped_buffer = slice.get_mapped_range();
+
+ mapped_buffer.chunks(dimensions.padded_bytes_per_row).fold(
+ vec![],
+ |mut acc, row| {
+ acc.extend(&row[..dimensions.unpadded_bytes_per_row]);
+ acc
+ },
+ )
+}
+
+#[derive(Clone, Copy, Debug)]
+struct BufferDimensions {
+ width: u32,
+ height: u32,
+ unpadded_bytes_per_row: usize,
+ padded_bytes_per_row: usize,
+}
+
+impl BufferDimensions {
+ fn new(size: Size<u32>) -> Self {
+ let unpadded_bytes_per_row = size.width as usize * 4; //slice of buffer per row; always RGBA
+ let alignment = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT as usize; //256
+ let padded_bytes_per_row_padding =
+ (alignment - unpadded_bytes_per_row % alignment) % alignment;
+ let padded_bytes_per_row =
+ unpadded_bytes_per_row + padded_bytes_per_row_padding;
+
+ Self {
+ width: size.width,
+ height: size.height,
+ unpadded_bytes_per_row,
+ padded_bytes_per_row,
+ }
+ }
}
diff --git a/winit/src/application.rs b/winit/src/application.rs
index 4147be17..5176bec6 100644
--- a/winit/src/application.rs
+++ b/winit/src/application.rs
@@ -19,7 +19,7 @@ use crate::graphics::compositor::{self, Compositor};
use crate::runtime::clipboard;
use crate::runtime::program::Program;
use crate::runtime::user_interface::{self, UserInterface};
-use crate::runtime::{Command, Debug};
+use crate::runtime::{Command, Debug, Screenshot};
use crate::style::application::{Appearance, StyleSheet};
use crate::{Clipboard, Error, Proxy, Settings};
@@ -308,6 +308,8 @@ async fn run_instance<A, E, C>(
run_command(
&application,
+ &mut compositor,
+ &mut surface,
&mut cache,
&state,
&mut renderer,
@@ -318,7 +320,6 @@ async fn run_instance<A, E, C>(
&mut proxy,
&mut debug,
&window,
- || compositor.fetch_information(),
);
runtime.track(application.subscription().into_recipes());
@@ -382,6 +383,8 @@ async fn run_instance<A, E, C>(
// Update application
update(
&mut application,
+ &mut compositor,
+ &mut surface,
&mut cache,
&state,
&mut renderer,
@@ -392,7 +395,6 @@ async fn run_instance<A, E, C>(
&mut debug,
&mut messages,
&window,
- || compositor.fetch_information(),
);
// Update window
@@ -645,8 +647,10 @@ where
/// Updates an [`Application`] by feeding it the provided messages, spawning any
/// resulting [`Command`], and tracking its [`Subscription`].
-pub fn update<A: Application, E: Executor>(
+pub fn update<A: Application, C, E: Executor>(
application: &mut A,
+ compositor: &mut C,
+ surface: &mut C::Surface,
cache: &mut user_interface::Cache,
state: &State<A>,
renderer: &mut A::Renderer,
@@ -657,8 +661,8 @@ pub fn update<A: Application, E: Executor>(
debug: &mut Debug,
messages: &mut Vec<A::Message>,
window: &winit::window::Window,
- graphics_info: impl FnOnce() -> compositor::Information + Copy,
) where
+ C: Compositor<Renderer = A::Renderer> + 'static,
<A::Renderer as core::Renderer>::Theme: StyleSheet,
{
for message in messages.drain(..) {
@@ -676,6 +680,8 @@ pub fn update<A: Application, E: Executor>(
run_command(
application,
+ compositor,
+ surface,
cache,
state,
renderer,
@@ -686,7 +692,6 @@ pub fn update<A: Application, E: Executor>(
proxy,
debug,
window,
- graphics_info,
);
}
@@ -695,8 +700,10 @@ pub fn update<A: Application, E: Executor>(
}
/// Runs the actions of a [`Command`].
-pub fn run_command<A, E>(
+pub fn run_command<A, C, E>(
application: &A,
+ compositor: &mut C,
+ surface: &mut C::Surface,
cache: &mut user_interface::Cache,
state: &State<A>,
renderer: &mut A::Renderer,
@@ -707,10 +714,10 @@ pub fn run_command<A, E>(
proxy: &mut winit::event_loop::EventLoopProxy<A::Message>,
debug: &mut Debug,
window: &winit::window::Window,
- _graphics_info: impl FnOnce() -> compositor::Information + Copy,
) where
A: Application,
E: Executor,
+ C: Compositor<Renderer = A::Renderer> + 'static,
<A::Renderer as core::Renderer>::Theme: StyleSheet,
{
use crate::runtime::command;
@@ -802,12 +809,28 @@ pub fn run_command<A, E>(
.send_event(tag(window.id().into()))
.expect("Send message to event loop");
}
+ window::Action::Screenshot(tag) => {
+ let bytes = compositor.screenshot(
+ renderer,
+ surface,
+ state.viewport(),
+ state.background_color(),
+ &debug.overlay(),
+ );
+
+ proxy
+ .send_event(tag(Screenshot::new(
+ bytes,
+ state.physical_size(),
+ )))
+ .expect("Send message to event loop.")
+ }
},
command::Action::System(action) => match action {
system::Action::QueryInformation(_tag) => {
#[cfg(feature = "system")]
{
- let graphics_info = _graphics_info();
+ let graphics_info = compositor.fetch_information();
let proxy = proxy.clone();
let _ = std::thread::spawn(move || {