wgpu_examples/
utils.rs

1#[cfg(not(target_arch = "wasm32"))]
2use std::io::Write;
3use std::time::Instant;
4#[cfg(target_arch = "wasm32")]
5use wasm_bindgen::prelude::*;
6
7#[cfg(target_arch = "wasm32")]
8fn get_content_div() -> web_sys::Element {
9    web_sys::window()
10        .and_then(|window| window.document())
11        .and_then(|document| document.get_element_by_id("content"))
12        .expect("Could not get document / content.")
13}
14
15/// Replaces the site body with a message telling the user to open the console and use that.
16#[cfg(target_arch = "wasm32")]
17pub fn add_web_nothing_to_see_msg() {
18    get_content_div().set_inner_html(
19        "<h1>This is a compute example, so there's nothing to see here. Open the console!</h1>",
20    );
21}
22
23/// Outputs a vector of RGBA bytes as a png image with the given dimensions on the given path.
24#[cfg(not(target_arch = "wasm32"))]
25pub fn output_image_native(image_data: Vec<u8>, texture_dims: (usize, usize), path: String) {
26    let mut png_data = Vec::<u8>::with_capacity(image_data.len());
27    let mut encoder = png::Encoder::new(
28        std::io::Cursor::new(&mut png_data),
29        texture_dims.0 as u32,
30        texture_dims.1 as u32,
31    );
32    encoder.set_color(png::ColorType::Rgba);
33    let mut png_writer = encoder.write_header().unwrap();
34    png_writer.write_image_data(&image_data[..]).unwrap();
35    png_writer.finish().unwrap();
36    log::info!("PNG file encoded in memory.");
37
38    let mut file = std::fs::File::create(&path).unwrap();
39    file.write_all(&png_data[..]).unwrap();
40    log::info!("PNG file written to disc as \"{path}\".");
41}
42
43/// Effectively a version of `output_image_native` but meant for web browser contexts.
44///
45/// This is achieved via in `img` element on the page. If the target image element does
46/// not exist, this function creates one. If it does, the image data is overridden.
47///
48/// This function makes use of a hidden staging canvas which the data is copied to in
49/// order to create a data URL.
50#[cfg(target_arch = "wasm32")]
51pub fn output_image_wasm(image_data: Vec<u8>, texture_dims: (usize, usize)) {
52    let document = web_sys::window().unwrap().document().unwrap();
53    let content_div = get_content_div();
54
55    let canvas = if let Some(found_canvas) = document.get_element_by_id("staging-canvas") {
56        match found_canvas.dyn_into::<web_sys::HtmlCanvasElement>() {
57            Ok(canvas_as_canvas) => canvas_as_canvas,
58            Err(e) => {
59                log::error!(
60                    "In searching for a staging canvas for outputting an image \
61                    (element with id \"staging-canvas\"), found non-canvas element: {e:?}.
62                    Replacing with standard staging canvas."
63                );
64                e.remove();
65                create_staging_canvas(&document)
66            }
67        }
68    } else {
69        log::info!("Output image staging canvas element not found; creating.");
70        create_staging_canvas(&document)
71    };
72    // Having the size attributes the right size is so important, we should always do it
73    // just to be safe. Also, what if we might want the image size to be able to change?
74    let image_dimension_strings = (texture_dims.0.to_string(), texture_dims.1.to_string());
75    canvas
76        .set_attribute("width", image_dimension_strings.0.as_str())
77        .unwrap();
78    canvas
79        .set_attribute("height", image_dimension_strings.1.as_str())
80        .unwrap();
81
82    let context = canvas
83        .get_context("2d")
84        .unwrap()
85        .unwrap()
86        .dyn_into::<web_sys::CanvasRenderingContext2d>()
87        .unwrap();
88    let image_data = web_sys::ImageData::new_with_u8_clamped_array(
89        wasm_bindgen::Clamped(&image_data),
90        texture_dims.0 as u32,
91    )
92    .unwrap();
93    context.put_image_data(&image_data, 0.0, 0.0).unwrap();
94
95    // Get the img element that will act as our target for rendering from the canvas.
96    let image_element = if let Some(found_image_element) =
97        document.get_element_by_id("output-image-target")
98    {
99        match found_image_element.dyn_into::<web_sys::HtmlImageElement>() {
100            Ok(e) => e,
101            Err(e) => {
102                log::error!(
103                    "Found an element with the id \"output-image-target\" but it was not an image: {e:?}.
104                    Replacing with default image output element.",
105                );
106                e.remove();
107                create_output_image_element(&document)
108            }
109        }
110    } else {
111        log::info!("Output image element not found; creating.");
112        create_output_image_element(&document)
113    };
114    // The canvas is currently the image we ultimately want. We can create a data url from it now.
115    let data_url = canvas.to_data_url().unwrap();
116    image_element.set_src(&data_url);
117    log::info!("Copied image from staging canvas to image element.");
118
119    if document.get_element_by_id("image-for-you-text").is_none() {
120        log::info!("\"Image for you\" text not found; creating.");
121        let p = document
122            .create_element("p")
123            .expect("Failed to create p element for \"image for you text\".");
124        p.set_text_content(Some(
125            "The above image is for you!
126        You can drag it to your desktop to download.",
127        ));
128        p.set_id("image-for-you-text");
129        content_div
130            .append_child(&p)
131            .expect("Failed to append \"image for you text\" to document.");
132    }
133}
134
135#[cfg(target_arch = "wasm32")]
136fn create_staging_canvas(document: &web_sys::Document) -> web_sys::HtmlCanvasElement {
137    let content_div = get_content_div();
138    let new_canvas = document
139        .create_element("canvas")
140        .expect("Failed to create staging canvas.")
141        .dyn_into::<web_sys::HtmlCanvasElement>()
142        .unwrap();
143    // We don't want to show the canvas, we just want it to exist in the background.
144    new_canvas.set_attribute("hidden", "true").unwrap();
145    new_canvas.set_attribute("background-color", "red").unwrap();
146    content_div.append_child(&new_canvas).unwrap();
147    log::info!("Created new staging canvas: {:?}", &new_canvas);
148    new_canvas
149}
150
151#[cfg(target_arch = "wasm32")]
152fn create_output_image_element(document: &web_sys::Document) -> web_sys::HtmlImageElement {
153    let content_div = get_content_div();
154    let new_image = document
155        .create_element("img")
156        .expect("Failed to create output image element.")
157        .dyn_into::<web_sys::HtmlImageElement>()
158        .unwrap();
159    new_image.set_id("output-image-target");
160    content_div.replace_children_with_node_1(&new_image);
161    log::info!("Created new output target image: {:?}", &new_image);
162    new_image
163}
164
165#[cfg(not(target_arch = "wasm32"))]
166/// If the environment variable `WGPU_ADAPTER_NAME` is set, this function will attempt to
167/// initialize the adapter with that name. If it is not set, it will attempt to initialize
168/// the adapter which supports the required features.
169pub(crate) async fn get_adapter_with_capabilities_or_from_env(
170    instance: &wgpu::Instance,
171    required_features: &wgpu::Features,
172    required_downlevel_capabilities: &wgpu::DownlevelCapabilities,
173    surface: &Option<&wgpu::Surface<'_>>,
174) -> wgpu::Adapter {
175    use wgpu::Backends;
176    if std::env::var("WGPU_ADAPTER_NAME").is_ok() {
177        let adapter = wgpu::util::initialize_adapter_from_env_or_default(instance, *surface)
178            .await
179            .expect("No suitable GPU adapters found on the system!");
180
181        let adapter_info = adapter.get_info();
182        log::info!("Using {} ({:?})", adapter_info.name, adapter_info.backend);
183
184        let adapter_features = adapter.features();
185        assert!(
186            adapter_features.contains(*required_features),
187            "Adapter does not support required features for this example: {:?}",
188            *required_features - adapter_features
189        );
190
191        let downlevel_capabilities = adapter.get_downlevel_capabilities();
192        assert!(
193            downlevel_capabilities.shader_model >= required_downlevel_capabilities.shader_model,
194            "Adapter does not support the minimum shader model required to run this example: {:?}",
195            required_downlevel_capabilities.shader_model
196        );
197        assert!(
198                downlevel_capabilities
199                    .flags
200                    .contains(required_downlevel_capabilities.flags),
201                "Adapter does not support the downlevel capabilities required to run this example: {:?}",
202                required_downlevel_capabilities.flags - downlevel_capabilities.flags
203            );
204        adapter
205    } else {
206        let adapters = instance.enumerate_adapters(Backends::all());
207
208        let mut chosen_adapter = None;
209        for adapter in adapters {
210            if let Some(surface) = surface {
211                if !adapter.is_surface_supported(surface) {
212                    continue;
213                }
214            }
215
216            let required_features = *required_features;
217            let adapter_features = adapter.features();
218            if !adapter_features.contains(required_features) {
219                continue;
220            } else {
221                chosen_adapter = Some(adapter);
222                break;
223            }
224        }
225
226        chosen_adapter.expect("No suitable GPU adapters found on the system!")
227    }
228}
229
230#[cfg(target_arch = "wasm32")]
231pub(crate) async fn get_adapter_with_capabilities_or_from_env(
232    instance: &wgpu::Instance,
233    required_features: &wgpu::Features,
234    required_downlevel_capabilities: &wgpu::DownlevelCapabilities,
235    surface: &Option<&wgpu::Surface<'_>>,
236) -> wgpu::Adapter {
237    let adapter = wgpu::util::initialize_adapter_from_env_or_default(instance, *surface)
238        .await
239        .expect("No suitable GPU adapters found on the system!");
240
241    let adapter_info = adapter.get_info();
242    log::info!("Using {} ({:?})", adapter_info.name, adapter_info.backend);
243
244    let adapter_features = adapter.features();
245    assert!(
246        adapter_features.contains(*required_features),
247        "Adapter does not support required features for this example: {:?}",
248        *required_features - adapter_features
249    );
250
251    let downlevel_capabilities = adapter.get_downlevel_capabilities();
252    assert!(
253        downlevel_capabilities.shader_model >= required_downlevel_capabilities.shader_model,
254        "Adapter does not support the minimum shader model required to run this example: {:?}",
255        required_downlevel_capabilities.shader_model
256    );
257    assert!(
258        downlevel_capabilities
259            .flags
260            .contains(required_downlevel_capabilities.flags),
261        "Adapter does not support the downlevel capabilities required to run this example: {:?}",
262        required_downlevel_capabilities.flags - downlevel_capabilities.flags
263    );
264    adapter
265}
266
267/// A custom timer that only starts counting after the first call to get its time value.
268/// Useful because some examples have animations that would otherwise get started at initialization
269/// leading to random CI fails.
270#[derive(Default)]
271pub struct AnimationTimer {
272    start_time: Option<Instant>,
273}
274
275impl AnimationTimer {
276    pub fn time(&mut self) -> f32 {
277        match self.start_time {
278            None => {
279                self.start_time = Some(Instant::now());
280                0.0
281            }
282            Some(ref instant) => instant.elapsed().as_secs_f32(),
283        }
284    }
285}