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            dx12_shader_compiler: wgpu::Dx12Compiler::default(),
134            flags: wgpu::InstanceFlags::default(),
135            gles_minor_version: wgpu::Gles3MinorVersion::Automatic,
136        });
137
138        //if we are on wasm we cannot enumerate adapter so we skip this at compile time
139        cfg_if::cfg_if! {
140                if #[cfg(not(target_arch = "wasm32"))]{
141                let adapters = enumerate_adapters(&instance);
142                info!("Number of possible adapters: {:?}", adapters.len());
143                for (i, adapter) in adapters.iter().enumerate() {
144                    info!("Adapter option {:?}: {:?}", i + 1, adapter.get_info());
145                }
146            }
147        }
148        let adapter = get_adapter(&instance, None);
149        info!("Selected adapter: {:?}", adapter.get_info());
150
151        // info!("{:?}", adapter.get_info());
152        //TODO if the adapter is not a discrete Nvidia gpu, disable the wgpu to pytorch
153        // interop
154
155        //features
156        let mut desired_features = wgpu::Features::empty();
157        cfg_if::cfg_if! {
158            if #[cfg(not(target_arch = "wasm32"))]{
159                // println!("compiling with time query");
160                desired_features = desired_features.union(wgpu::Features::TIMESTAMP_QUERY.union(wgpu::Features::TIMESTAMP_QUERY_INSIDE_PASSES));
161                desired_features = desired_features.union(wgpu::Features::POLYGON_MODE_POINT);
162                desired_features = desired_features.union(wgpu::Features::POLYGON_MODE_LINE);
163            }
164        }
165        // There would not really be a situation where we use ViewerHeadless on the web, so this can be
166        // either within the above cfg_if or here. I have put this here for consistency with the code in Viewer.rs
167        // desired_features = desired_features.union(wgpu::Features::DEPTH32FLOAT_STENCIL8);
168        // let required_features = adapter.features().intersection(desired_features); //only take the features that are actually supported
169        let mut required_features = adapter.features().intersection(desired_features); //only take the features that are actually supported
170        required_features = required_features.union(wgpu::Features::DEPTH32FLOAT_STENCIL8);
171        info!("Enabled Features: {required_features:?}");
172
173        //dealing with wasm putting 2048 as maximum texture size
174        //https://github.com/gfx-rs/wgpu/discussions/2952
175        let max_limits = adapter.limits();
176        #[allow(unused_mut)]
177        let mut limits_to_request = wgpu::Limits::default();
178        if cfg!(target_arch = "wasm32") {
179            limits_to_request = wgpu::Limits::downlevel_webgl2_defaults();
180        }
181        limits_to_request.max_texture_dimension_1d = max_limits.max_texture_dimension_1d;
182        limits_to_request.max_texture_dimension_2d = max_limits.max_texture_dimension_2d;
183        limits_to_request.max_buffer_size = max_limits.max_buffer_size;
184
185        let mut memory_hints = wgpu::MemoryHints::Performance;
186        if cfg!(target_arch = "wasm32") {
187            //we usually have issue with running out of memory on wasm, so I would rather
188            // optimize for low memory usage
189            memory_hints = wgpu::MemoryHints::MemoryUsage;
190        }
191
192        let (device, queue) = adapter
193            .request_device(
194                &wgpu::DeviceDescriptor {
195                    label: None,
196                    required_features,
197                    required_limits: limits_to_request,
198                    memory_hints,
199                },
200                None, // Trace path
201            )
202            .block_on()
203            .expect("A device and queue could not be created. Maybe there's a driver issue on your machine?");
204
205        let runner = RunnerHeadless::default();
206
207        let gpu = Gpu::new(adapter, instance, device, queue);
208        let mut scene = Scene::new();
209        let camera = Camera::new(GLOSS_CAM_NAME, &mut scene, false);
210        let _ = scene.world.insert_one(
211            camera.entity,
212            TargetResolution {
213                width,
214                height,
215                update_mode: TargetResolutionUpdate::Fixed,
216            },
217        );
218        let renderer = Renderer::new(&gpu, &config.render, None); //moves the device and queue into it and takes ownership of them
219
220        Self {
221            gpu,
222            renderer,
223            camera,
224            scene,
225            plugins: Plugins::new(),
226            config: config.clone(),
227            runner,
228        }
229    }
230
231    pub fn insert_plugin<T: Plugin + 'static>(&mut self, plugin: &T) {
232        self.plugins.insert_plugin(plugin);
233    }
234    #[allow(clippy::missing_panics_doc)]
235    pub fn run_manual_plugins(&mut self) {
236        self.plugins.run_logic_systems_headless(&mut self.scene, &mut self.runner, false);
237    }
238    //wasm cannot compile the run_return() call so we just disable this whole
239    // function
240    pub fn update(&mut self) {
241        self.render(None);
242    }
243
244    pub fn render_from_cam(&mut self, cam: &mut Camera) {
245        //we replace our defaul
246        self.render(Some(cam));
247    }
248
249    //called at the beggining of the render and sets the time that all systems will
250    // use
251    pub fn start_frame(&mut self) -> Duration {
252        //first time we call this we do a warmup render to initialize everything
253        if !self.runner.did_warmup {
254            self.runner.did_warmup = true; //has to be put here because warmup actually calls start_frame and we don't
255                                           // want an infinite recurrsion
256            self.warmup();
257            self.warmup();
258        }
259
260        self.runner.update_dt();
261        debug!("after update dt it is {:?}", self.runner.dt());
262        self.runner.time_last_frame = Instant::now();
263
264        self.runner.frame_is_started = true;
265
266        self.runner.dt
267    }
268
269    fn render(&mut self, other_cam: Option<&mut Camera>) {
270        if self.runner.first_time {
271            self.runner.time_init = Instant::now();
272        }
273
274        self.plugins.run_logic_systems_headless(&mut self.scene, &mut self.runner, true);
275
276        let dt = self.runner.dt();
277
278        //we render to an internal texture since we have no surface
279        // let out_view = self.renderer.rendered_tex().view;
280
281        self.renderer
282            .render_to_texture(&self.gpu, other_cam.unwrap_or(&mut self.camera), &mut self.scene, &mut self.config, dt);
283
284        self.runner.first_time = false;
285        self.runner.frame_is_started = false;
286    }
287
288    pub fn warmup(&mut self) {
289        debug!("Starting warmup");
290        self.start_frame();
291        self.run_manual_plugins(); //auto plugins will run when we do self.render(), but here we also need to run
292                                   // the manual ones
293        #[cfg(not(target_arch = "wasm32"))] //wasm cannot do update because it needs run_return
294        self.update();
295        #[cfg(target_arch = "wasm32")] //
296        self.render();
297        self.reset_for_first_time();
298        debug!("finished warmup");
299    }
300
301    pub fn reset_for_first_time(&mut self) {
302        self.runner.first_time = true;
303    }
304
305    pub fn get_final_tex(&self) -> &Texture {
306        let tex = self.renderer.rendered_tex();
307        tex
308    }
309
310    pub fn get_final_depth(&self) -> &Texture {
311        let depth = self.renderer.depth_buffer();
312        depth
313    }
314
315    pub fn set_size(&mut self, width: u32, height: u32) {
316        self.camera.set_target_res(width, height, &mut self.scene);
317    }
318}