gloss_renderer/
viewer_headless.rs

1cfg_if::cfg_if! {
2    if #[cfg(not(target_arch = "wasm32"))] {
3        use crate::components::{TargetResolution, TargetResolutionUpdate};
4        use crate::config::Config;
5        use crate::forward_renderer::renderer::Renderer;
6        use crate::logger::gloss_setup_logger_from_config;
7        use crate::plugin_manager::plugins::{Plugin, Plugins};
8        use crate::scene::Scene;
9        use crate::set_panic_hook;
10        use crate::viewer::supported_backends;
11        use crate::{camera::Camera, scene::GLOSS_CAM_NAME};
12
13        use easy_wgpu::gpu::Gpu;
14        use easy_wgpu::texture::Texture;
15
16        use log::debug;
17        use pollster::FutureExt;
18    }
19}
20// #[cfg(target_arch = "wasm32")]
21// use winit::platform::web::EventLoopExtWebSys;
22
23use core::time::Duration;
24#[allow(unused_imports)]
25use log::{error, info, Level};
26
27#[cfg(target_arch = "wasm32")]
28#[allow(unused_imports)]
29use wasm_bindgen::prelude::*;
30use wasm_timer::Instant;
31
32#[cfg(not(target_arch = "wasm32"))]
33use crate::viewer::enumerate_adapters;
34#[cfg(not(target_arch = "wasm32"))]
35use crate::viewer::get_adapter;
36
37#[derive(Debug)]
38#[repr(C)]
39pub struct RunnerHeadless {
40    pub is_running: bool,
41    pub do_render: bool,
42    pub first_time: bool,
43    pub did_warmup: bool,
44    frame_is_started: bool, /* after calling viewer.start_frame() this gets set to true. any subsequent cals to start frame will be ignored if
45                             * this frame has already been started. This can happen when using python and doing viewer.update() this will run some
46                             * system that does a window.request_redraw which will cause another rerender but this frame has already started so we
47                             * don't need to do anything since we are already in the middle of redrawing */
48    time_init: Instant,       //time when the init of the viewer has finished
49    time_last_frame: Instant, //time when we started the previous frame. So the instant when we called viewer.start_frame()
50    dt: Duration,             /* delta time since we finished last frame, we store it directly here instead of constantly querying it because
51                               * different systems might then get different dt depending on how long they take to run. This dt is set once when
52                               * doing viewer.start_frame() */
53}
54impl Default for RunnerHeadless {
55    fn default() -> Self {
56        let time_init = Instant::now();
57        let time_last_frame = Instant::now();
58        Self {
59            is_running: false,
60            do_render: true,
61            first_time: true,
62            did_warmup: false,
63            frame_is_started: false,
64            time_init,
65            time_last_frame,
66            dt: Duration::ZERO,
67        }
68    }
69}
70#[allow(unused)]
71impl RunnerHeadless {
72    pub fn time_since_init(&self) -> Duration {
73        if self.first_time {
74            Duration::ZERO
75        } else {
76            self.time_init.elapsed()
77        }
78    }
79    pub fn update_dt(&mut self) {
80        if self.first_time {
81            self.dt = Duration::ZERO;
82        } else {
83            self.dt = self.time_last_frame.elapsed();
84        }
85    }
86    pub fn dt(&self) -> Duration {
87        self.dt
88    }
89}
90
91/// `ViewerHeadless` performs similarly as [Viewer] with the difference that it
92/// can be used when rendering on headless machines. A typical usage is
93/// rendering an image using [`ViewerHeadless::update`], recovering the texture
94/// using [`ViewerHeadless::get_final_tex`], getting it to CPU using
95/// [`Texture::download_to_cpu`] and finally write it to disk using
96/// [`ImageBuffer::save`]
97#[cfg(not(target_arch = "wasm32"))] //wasm cannot compile the run_return() call so we just disable the whole
98                                    // headless viewer
99pub struct ViewerHeadless {
100    // have the renderer separate
101    pub renderer: Renderer,
102    pub camera: Camera,
103    pub scene: Scene,
104    pub plugins: Plugins,
105    pub config: Config,
106    pub runner: RunnerHeadless,
107    // The order of properties in a struct is the order in which items are dropped.
108    // wgpu seems to require that the device be dropped last, otherwise there is a resouce
109    // leak.
110    pub gpu: Gpu,
111}
112
113#[cfg(not(target_arch = "wasm32"))]
114impl ViewerHeadless {
115    pub fn new(width: u32, height: u32, config_path: Option<&str>) -> Self {
116        let config = Config::new(config_path);
117        Self::new_with_config(width, height, &config)
118    }
119
120    #[allow(clippy::too_many_lines)]
121    #[allow(clippy::missing_panics_doc)]
122    pub fn new_with_config(width: u32, height: u32, config: &Config) -> Self {
123        set_panic_hook();
124        if config.core.auto_create_logger {
125            gloss_setup_logger_from_config(config);
126        }
127
128        //expensive but useful
129        re_memory::accounting_allocator::set_tracking_callstacks(config.core.enable_memory_profiling_callstacks);
130
131        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
132            backends: supported_backends(),
133            flags: wgpu::InstanceFlags::default(),
134            backend_options: wgpu::BackendOptions::default(),
135        });
136
137        //if we are on wasm we cannot enumerate adapter so we skip this at compile time
138        cfg_if::cfg_if! {
139                if #[cfg(not(target_arch = "wasm32"))]{
140                let adapters = enumerate_adapters(&instance);
141                info!("Number of possible adapters: {:?}", adapters.len());
142                for (i, adapter) in adapters.iter().enumerate() {
143                    info!("Adapter option {:?}: {:?}", i + 1, adapter.get_info());
144                }
145            }
146        }
147        let adapter = get_adapter(&instance, None).block_on();
148        info!("Selected adapter: {:?}", adapter.get_info());
149
150        // info!("{:?}", adapter.get_info());
151        //TODO if the adapter is not a discrete Nvidia gpu, disable the wgpu to pytorch
152        // interop
153
154        //features
155        let mut desired_features = wgpu::Features::empty();
156        cfg_if::cfg_if! {
157            if #[cfg(not(target_arch = "wasm32"))]{
158                // println!("compiling with time query");
159                desired_features = desired_features.union(wgpu::Features::TIMESTAMP_QUERY.union(wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES));
160                desired_features = desired_features.union(wgpu::Features::POLYGON_MODE_POINT);
161                desired_features = desired_features.union(wgpu::Features::POLYGON_MODE_LINE);
162            }
163        }
164        // There would not really be a situation where we use ViewerHeadless on the web, so this can be
165        // either within the above cfg_if or here. I have put this here for consistency with the code in Viewer.rs
166        // desired_features = desired_features.union(wgpu::Features::DEPTH32FLOAT_STENCIL8);
167        // let required_features = adapter.features().intersection(desired_features); //only take the features that are actually supported
168        let mut required_features = adapter.features().intersection(desired_features); //only take the features that are actually supported
169        required_features = required_features.union(wgpu::Features::DEPTH32FLOAT_STENCIL8);
170        info!("Enabled Features: {required_features:?}");
171
172        //dealing with wasm putting 2048 as maximum texture size
173        //https://github.com/gfx-rs/wgpu/discussions/2952
174        let max_limits = adapter.limits();
175        #[allow(unused_mut)]
176        let mut limits_to_request = wgpu::Limits::default();
177        if cfg!(target_arch = "wasm32") {
178            limits_to_request = wgpu::Limits::downlevel_webgl2_defaults();
179        }
180        limits_to_request.max_texture_dimension_1d = max_limits.max_texture_dimension_1d;
181        limits_to_request.max_texture_dimension_2d = max_limits.max_texture_dimension_2d;
182        limits_to_request.max_buffer_size = max_limits.max_buffer_size;
183
184        let mut memory_hints = wgpu::MemoryHints::Performance;
185        if cfg!(target_arch = "wasm32") {
186            //we usually have issue with running out of memory on wasm, so I would rather
187            // optimize for low memory usage
188            memory_hints = wgpu::MemoryHints::MemoryUsage;
189        }
190
191        let (device, queue) = adapter
192            .request_device(&wgpu::DeviceDescriptor {
193                label: None,
194                required_features,
195                required_limits: limits_to_request,
196                memory_hints,
197                trace: wgpu::Trace::Off,
198            })
199            .block_on()
200            .expect("A device and queue could not be created. Maybe there's a driver issue on your machine?");
201
202        let runner = RunnerHeadless::default();
203
204        let gpu = Gpu::new(adapter, instance, device, queue);
205        let mut scene = Scene::new();
206        let camera = Camera::new(GLOSS_CAM_NAME, &mut scene, false);
207        let _ = scene.world.insert_one(
208            camera.entity,
209            TargetResolution {
210                width,
211                height,
212                update_mode: TargetResolutionUpdate::Fixed,
213            },
214        );
215        let renderer = Renderer::new(&gpu, &config.render, None); //moves the device and queue into it and takes ownership of them
216
217        Self {
218            gpu,
219            renderer,
220            camera,
221            scene,
222            plugins: Plugins::new(),
223            config: config.clone(),
224            runner,
225        }
226    }
227
228    pub fn insert_plugin<T: Plugin + 'static>(&mut self, plugin: &T) {
229        self.plugins.insert_plugin(plugin);
230    }
231    #[allow(clippy::missing_panics_doc)]
232    pub fn run_manual_plugins(&mut self) {
233        self.plugins.run_logic_systems_headless(&mut self.scene, &mut self.runner, false);
234    }
235    //wasm cannot compile the run_return() call so we just disable this whole
236    // function
237    pub fn update(&mut self) {
238        self.render(None);
239    }
240
241    pub fn render_from_cam(&mut self, cam: &mut Camera) {
242        //we replace our defaul
243        self.render(Some(cam));
244    }
245
246    //called at the beggining of the render and sets the time that all systems will
247    // use
248    pub fn start_frame(&mut self) -> Duration {
249        //first time we call this we do a warmup render to initialize everything
250        if !self.runner.did_warmup {
251            self.runner.did_warmup = true; //has to be put here because warmup actually calls start_frame and we don't
252                                           // want an infinite recurrsion
253            self.warmup();
254            self.warmup();
255        }
256
257        self.runner.update_dt();
258        debug!("after update dt it is {:?}", self.runner.dt());
259        self.runner.time_last_frame = Instant::now();
260
261        self.runner.frame_is_started = true;
262
263        self.runner.dt
264    }
265
266    fn render(&mut self, other_cam: Option<&mut Camera>) {
267        if self.runner.first_time {
268            self.runner.time_init = Instant::now();
269        }
270
271        self.plugins.run_logic_systems_headless(&mut self.scene, &mut self.runner, true);
272
273        let dt = self.runner.dt();
274
275        //we render to an internal texture since we have no surface
276        // let out_view = self.renderer.rendered_tex().view;
277
278        self.renderer
279            .render_to_texture(&self.gpu, other_cam.unwrap_or(&mut self.camera), &mut self.scene, &mut self.config, dt);
280
281        self.runner.first_time = false;
282        self.runner.frame_is_started = false;
283    }
284
285    pub fn warmup(&mut self) {
286        debug!("Starting warmup");
287        self.start_frame();
288        self.run_manual_plugins(); //auto plugins will run when we do self.render(), but here we also need to run
289                                   // the manual ones
290        #[cfg(not(target_arch = "wasm32"))] //wasm cannot do update because it needs run_return
291        self.update();
292        #[cfg(target_arch = "wasm32")] //
293        self.render();
294        self.reset_for_first_time();
295        debug!("finished warmup");
296    }
297
298    pub fn reset_for_first_time(&mut self) {
299        self.runner.first_time = true;
300    }
301
302    pub fn get_final_tex(&self) -> &Texture {
303        let tex = self.renderer.rendered_tex();
304        tex
305    }
306
307    pub fn get_final_depth(&self) -> &Texture {
308        let depth = self.renderer.depth_buffer();
309        depth
310    }
311
312    pub fn set_size(&mut self, width: u32, height: u32) {
313        self.camera.set_target_res(width, height, &mut self.scene);
314    }
315}