Skip to main content

ranim_render/
lib.rs

1//! Rendering stuff in ranim
2#![recursion_limit = "256"]
3// #![warn(missing_docs)]
4#![cfg_attr(docsrs, feature(doc_cfg))]
5#![allow(rustdoc::private_intra_doc_links)]
6#![doc(
7    html_logo_url = "https://raw.githubusercontent.com/AzurIce/ranim/refs/heads/main/assets/ranim.svg",
8    html_favicon_url = "https://raw.githubusercontent.com/AzurIce/ranim/refs/heads/main/assets/ranim.svg"
9)]
10/// Lightweight CPU timing spans, drained per frame by the preview
11/// profiler panel.
12pub mod cpu_probe;
13pub mod extract;
14/// The pipelines
15pub mod pipelines;
16/// The basic renderable structs
17pub mod primitives;
18pub mod resource;
19/// Render schedules and ordered extension points.
20pub mod schedule;
21mod setup;
22/// Upload instrumentation and experimental upload strategies
23/// (enabled via `RANIM_PROFILE_UPLOAD`).
24pub mod upload_probe;
25/// Rendering related utils
26pub mod utils;
27pub mod world;
28
29use bevy_app::{App, PluginsState, SubApp};
30use bevy_derive::AppLabel;
31use bevy_ecs::prelude::*;
32use glam::{UVec3, uvec3};
33use ranim_core::frame::EvaluatedFrame;
34
35pub use setup::RenderSetup;
36
37use crate::{
38    primitives::viewport::{ViewportGpuPacket, ViewportUniform},
39    resource::{PipelinesPool, RenderTextures},
40    schedule::{FrameTarget, RenderDimensions, RenderProfiler, install_schedules},
41    utils::{WgpuBuffer, WgpuVecBuffer},
42};
43use utils::WgpuContext;
44
45pub mod profiling_utils {
46    use wgpu_profiler::GpuTimerQueryResult;
47
48    /// Print a `GpuTimerQueryResult` tree (label + duration) to stdout.
49    pub fn scopes_to_console_recursive(results: &[GpuTimerQueryResult], indentation: u32) {
50        for scope in results {
51            if indentation > 0 {
52                print!("{:<width$}", "|", width = 4);
53            }
54
55            if let Some(time) = &scope.time {
56                println!(
57                    "{:.3}μs - {}",
58                    (time.end - time.start) * 1000.0 * 1000.0,
59                    scope.label
60                );
61            } else {
62                println!("n/a - {}", scope.label);
63            }
64
65            if !scope.nested_queries.is_empty() {
66                scopes_to_console_recursive(&scope.nested_queries, indentation + 1);
67            }
68        }
69    }
70}
71
72/// The [`App`] label of a ranim render sub-app.
73///
74/// Installed by [`install_render_app`]; read it back with
75/// `app.get_sub_app(RanimRenderApp)`.
76#[derive(AppLabel, Debug, Clone, Copy, PartialEq, Eq, Hash)]
77pub struct RanimRenderApp;
78
79/// Create an independent render runtime with the shared frame, viewport, and
80/// OIT infrastructure installed.
81///
82/// Item plugins install their own extraction and GPU systems on top.
83/// Extraction systems can read the current [`EvaluatedFrame`] resource during
84/// [`RenderSchedule::Extract`](crate::schedule::RenderSchedule::Extract).
85/// Plugins configure the render world directly; no logic world is created.
86pub fn build_render_app(ctx: &WgpuContext, width: u32, height: u32, oit_layers: usize) -> SubApp {
87    let mut render_app = SubApp::new();
88    render_app
89        .insert_resource(ctx.clone())
90        .insert_resource(RenderDimensions { width, height })
91        .insert_resource(ResolutionInfo::new(ctx, width, height, oit_layers))
92        .insert_resource(ViewportGpuPacket::new(ctx, &ViewportUniform::default()))
93        .insert_resource(RenderProfiler::new(ctx))
94        .init_resource::<PipelinesPool>()
95        .init_resource::<EvaluatedFrame>();
96    install_schedules(&mut render_app);
97    render_app
98}
99
100/// Build and install the render sub-app for `app`, with the project's
101/// [`RenderSetup`] applied.
102///
103/// On every [`App::update`], right after the main schedule ran, the sub-app's
104/// extract hook swaps the main world's current [`EvaluatedFrame`] into the
105/// render world (the render world's previous batch storage moves back, so
106/// both sides keep reusing their allocations).
107///
108/// # Panics
109///
110/// Panics if a plugin is not ready after installation. This supports
111/// synchronous setup using the already initialized GPU context.
112pub fn install_render_app(
113    app: &mut App,
114    ctx: &WgpuContext,
115    width: u32,
116    height: u32,
117    oit_layers: usize,
118    setup: RenderSetup,
119) {
120    let mut render_app = build_render_app(ctx, width, height, oit_layers);
121    render_app.add_plugins(setup);
122    assert_eq!(
123        render_app.plugins_state(),
124        PluginsState::Ready,
125        "render plugins must be ready after synchronous installation"
126    );
127    render_app.finish();
128    render_app.cleanup();
129    render_app.set_extract(|main, render| {
130        let Some(mut frame) = main.get_resource_mut::<EvaluatedFrame>() else {
131            return;
132        };
133        std::mem::swap(&mut *frame, &mut *render.resource_mut::<EvaluatedFrame>());
134    });
135    app.insert_sub_app(RanimRenderApp, render_app);
136}
137
138/// Manual driving of a standalone render sub-app built by [`build_render_app`].
139pub trait RenderAppExt {
140    /// Point the render world at the frame's output target.
141    ///
142    /// Cheap: the frame target clones the textures' views. Inserted per frame
143    /// when the output rotates over multiple readback buffers.
144    fn set_frame_target(&mut self, render_textures: &RenderTextures, clear_color: wgpu::Color);
145
146    /// Extract and render one evaluated frame into `render_textures`.
147    ///
148    /// The frame is temporarily swapped into the render world, then returned
149    /// with its input data intact so its batches can be reused or rendered
150    /// again. Extraction systems should only read the frame.
151    fn render_frame(
152        &mut self,
153        render_textures: &mut RenderTextures,
154        clear_color: wgpu::Color,
155        frame: &mut EvaluatedFrame,
156    );
157
158    /// Take the GPU timer scopes recorded for the most recent processed
159    /// frame (empty unless GPU timers are enabled and the device supports
160    /// timestamp queries).
161    fn take_last_gpu_scopes(&mut self) -> Option<Vec<wgpu_profiler::GpuTimerQueryResult>>;
162
163    /// Whether the device supports GPU timestamp queries.
164    fn gpu_timers_supported(&self) -> bool;
165
166    /// Whether GPU timer scopes are currently being recorded.
167    fn gpu_timers_enabled(&self) -> bool;
168
169    /// Enable/disable GPU timer recording at runtime. While enabled, each
170    /// frame pays a device poll to read the timers back.
171    fn set_gpu_timers_enabled(&self, on: bool);
172}
173
174impl RenderAppExt for SubApp {
175    fn set_frame_target(&mut self, render_textures: &RenderTextures, clear_color: wgpu::Color) {
176        self.insert_resource(FrameTarget::new(render_textures, clear_color));
177    }
178
179    fn render_frame(
180        &mut self,
181        render_textures: &mut RenderTextures,
182        clear_color: wgpu::Color,
183        frame: &mut EvaluatedFrame,
184    ) {
185        std::mem::swap(
186            frame,
187            &mut self.world_mut().resource_mut::<EvaluatedFrame>(),
188        );
189        self.insert_resource(FrameTarget::new(render_textures, clear_color));
190        self.update();
191        std::mem::swap(
192            frame,
193            &mut self.world_mut().resource_mut::<EvaluatedFrame>(),
194        );
195    }
196
197    fn take_last_gpu_scopes(&mut self) -> Option<Vec<wgpu_profiler::GpuTimerQueryResult>> {
198        self.world_mut()
199            .resource_mut::<RenderProfiler>()
200            .last_frame_scopes
201            .take()
202    }
203
204    fn gpu_timers_supported(&self) -> bool {
205        self.world().resource::<RenderProfiler>().inner.is_some()
206    }
207
208    fn gpu_timers_enabled(&self) -> bool {
209        self.world().resource::<RenderProfiler>().is_enabled()
210    }
211
212    fn set_gpu_timers_enabled(&self, on: bool) {
213        self.world().resource::<RenderProfiler>().set_enabled(on);
214    }
215}
216
217#[allow(unused)]
218#[derive(Resource)]
219pub struct ResolutionInfo {
220    buffer: WgpuBuffer<UVec3>,
221    pub(crate) pixel_count_buffer: WgpuVecBuffer<u32>,
222    oit_colors_buffer: WgpuVecBuffer<u32>,
223    oit_depths_buffer: WgpuVecBuffer<f32>,
224    bind_group: wgpu::BindGroup,
225}
226
227impl ResolutionInfo {
228    /// Shared per-pixel OIT storage bound by item rendering pipelines.
229    pub fn bind_group(&self) -> &wgpu::BindGroup {
230        &self.bind_group
231    }
232
233    pub fn new(ctx: &WgpuContext, width: u32, height: u32, oit_layers: usize) -> Self {
234        let buffer = WgpuBuffer::new_init(
235            ctx,
236            Some("ResolutionInfo Buffer"),
237            wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
238            uvec3(width, height, oit_layers as u32),
239        );
240
241        let pixel_count = (width * height) as usize;
242        let total_nodes = pixel_count * oit_layers;
243
244        let pixel_count_buffer = WgpuVecBuffer::new(
245            ctx,
246            Some("OIT Pixel Count Buffer"),
247            wgpu::BufferUsages::STORAGE
248                | wgpu::BufferUsages::COPY_DST
249                | wgpu::BufferUsages::COPY_SRC,
250            pixel_count,
251        );
252        let oit_colors_buffer = WgpuVecBuffer::new(
253            ctx,
254            Some("OIT Colors Buffer"),
255            wgpu::BufferUsages::STORAGE
256                | wgpu::BufferUsages::COPY_SRC
257                | wgpu::BufferUsages::COPY_DST,
258            total_nodes,
259        );
260        let oit_depths_buffer = WgpuVecBuffer::new(
261            ctx,
262            Some("OIT Depths Buffer"),
263            wgpu::BufferUsages::STORAGE
264                | wgpu::BufferUsages::COPY_SRC
265                | wgpu::BufferUsages::COPY_DST,
266            total_nodes,
267        );
268
269        let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
270            label: Some("ResolutionInfo BindGroup"),
271            layout: &Self::create_bind_group_layout(ctx),
272            entries: &[
273                wgpu::BindGroupEntry {
274                    binding: 0,
275                    resource: buffer.as_ref().as_entire_binding(),
276                },
277                wgpu::BindGroupEntry {
278                    binding: 1,
279                    resource: wgpu::BindingResource::Buffer(
280                        pixel_count_buffer.buffer.as_entire_buffer_binding(),
281                    ),
282                },
283                wgpu::BindGroupEntry {
284                    binding: 2,
285                    resource: wgpu::BindingResource::Buffer(
286                        oit_colors_buffer.buffer.as_entire_buffer_binding(),
287                    ),
288                },
289                wgpu::BindGroupEntry {
290                    binding: 3,
291                    resource: wgpu::BindingResource::Buffer(
292                        oit_depths_buffer.buffer.as_entire_buffer_binding(),
293                    ),
294                },
295            ],
296        });
297
298        Self {
299            buffer,
300            bind_group,
301            oit_colors_buffer,
302            oit_depths_buffer,
303            pixel_count_buffer,
304        }
305    }
306    // This may never be used?
307    // pub fn update(&mut self, ctx: &WgpuContext, resolution: UVec2) {
308    //     self.buffer.set(ctx, resolution);
309
310    //     let pixel_count = (data.screen_size[0] * data.screen_size[1]) as usize;
311    //     let layers = data.oit_layers as usize;
312    //     let total_nodes = pixel_count * layers;
313
314    //     let mut bind_group_dirty = false;
315
316    //     if self.pixel_count_buffer.len() != pixel_count {
317    //         self.pixel_count_buffer.resize(ctx, pixel_count);
318    //         bind_group_dirty = true;
319    //     }
320
321    //     if self.oit_colors_buffer.len() != total_nodes {
322    //         self.oit_colors_buffer.resize(ctx, total_nodes);
323    //         bind_group_dirty = true;
324    //     }
325
326    //     if self.oit_depths_buffer.len() != total_nodes {
327    //         self.oit_depths_buffer.resize(ctx, total_nodes);
328    //         bind_group_dirty = true;
329    //     }
330
331    //     if bind_group_dirty {
332    //         self.uniforms_bind_group = ViewportBindGroup::new(
333    //             ctx,
334    //             &self.uniforms_buffer,
335    //             &self.pixel_count_buffer,
336    //             &self.oit_colors_buffer,
337    //             &self.oit_depths_buffer,
338    //         );
339    //     }
340    // }
341    pub fn create_bind_group_layout(ctx: &WgpuContext) -> wgpu::BindGroupLayout {
342        ctx.device
343            .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
344                label: Some("ResolutionInfo BindGroupLayout"),
345                entries: &[
346                    wgpu::BindGroupLayoutEntry {
347                        binding: 0,
348                        visibility: wgpu::ShaderStages::VERTEX_FRAGMENT
349                            | wgpu::ShaderStages::COMPUTE,
350                        ty: wgpu::BindingType::Buffer {
351                            ty: wgpu::BufferBindingType::Uniform,
352                            has_dynamic_offset: false,
353                            min_binding_size: None,
354                        },
355                        count: None,
356                    },
357                    wgpu::BindGroupLayoutEntry {
358                        binding: 1,
359                        visibility: wgpu::ShaderStages::FRAGMENT,
360                        ty: wgpu::BindingType::Buffer {
361                            ty: wgpu::BufferBindingType::Storage { read_only: false },
362                            has_dynamic_offset: false,
363                            min_binding_size: None,
364                        },
365                        count: None,
366                    },
367                    wgpu::BindGroupLayoutEntry {
368                        binding: 2,
369                        visibility: wgpu::ShaderStages::FRAGMENT,
370                        ty: wgpu::BindingType::Buffer {
371                            ty: wgpu::BufferBindingType::Storage { read_only: false },
372                            has_dynamic_offset: false,
373                            min_binding_size: None,
374                        },
375                        count: None,
376                    },
377                    wgpu::BindGroupLayoutEntry {
378                        binding: 3,
379                        visibility: wgpu::ShaderStages::FRAGMENT,
380                        ty: wgpu::BindingType::Buffer {
381                            ty: wgpu::BufferBindingType::Storage { read_only: false },
382                            has_dynamic_offset: false,
383                            min_binding_size: None,
384                        },
385                        count: None,
386                    },
387                ],
388            })
389    }
390}