Added offscreen rendering support for wgpu & tiny-skia exposed with the window::screenshot command.

This commit is contained in:
Bingus 2023-03-25 10:45:39 -07:00 committed by Héctor Ramón Jiménez
parent c15f1b5f65
commit 233196eb14
No known key found for this signature in database
GPG key ID: 140CC052C94F138E
16 changed files with 893 additions and 24 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

80
runtime/src/screenshot.rs Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -46,6 +46,7 @@ pub mod geometry;
mod backend;
mod buffer;
mod offscreen;
mod quad;
mod text;
mod triangle;

102
wgpu/src/offscreen.rs Normal file
View file

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

View file

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

View file

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

View file

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

View file

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