use std::cell::{Cell, RefCell};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use astrelis_core::profiling::{profile_function, profile_scope};
use astrelis_winit::window::WinitWindow;
use crate::Color;
use crate::context::GraphicsContext;
use crate::framebuffer::Framebuffer;
use crate::gpu_profiling::GpuFrameProfiler;
use crate::target::RenderTarget;
#[derive(Debug, Clone, Copy, Default)]
pub struct FrameStats {
pub passes: usize,
pub draw_calls: usize,
}
pub struct AtomicFrameStats {
passes: AtomicU32,
draw_calls: AtomicU32,
}
impl AtomicFrameStats {
pub fn new() -> Self {
Self {
passes: AtomicU32::new(0),
draw_calls: AtomicU32::new(0),
}
}
pub fn increment_passes(&self) {
self.passes.fetch_add(1, Ordering::Relaxed);
}
pub fn increment_draw_calls(&self) {
self.draw_calls.fetch_add(1, Ordering::Relaxed);
}
pub fn passes(&self) -> u32 {
self.passes.load(Ordering::Relaxed)
}
pub fn draw_calls(&self) -> u32 {
self.draw_calls.load(Ordering::Relaxed)
}
pub fn to_frame_stats(&self) -> FrameStats {
FrameStats {
passes: self.passes() as usize,
draw_calls: self.draw_calls() as usize,
}
}
}
impl Default for AtomicFrameStats {
fn default() -> Self {
Self::new()
}
}
pub struct Surface {
pub(crate) texture: wgpu::SurfaceTexture,
pub(crate) view: wgpu::TextureView,
}
impl Surface {
pub fn texture(&self) -> &wgpu::Texture {
&self.texture.texture
}
pub fn view(&self) -> &wgpu::TextureView {
&self.view
}
}
pub struct Frame<'w> {
pub(crate) window: &'w crate::window::RenderWindow,
pub(crate) surface: Option<Surface>,
pub(crate) command_buffers: RefCell<Vec<wgpu::CommandBuffer>>,
pub(crate) stats: Arc<AtomicFrameStats>,
pub(crate) submitted: Cell<bool>,
pub(crate) surface_format: wgpu::TextureFormat,
pub(crate) gpu_profiler: Option<Arc<GpuFrameProfiler>>,
pub(crate) winit_window: Arc<WinitWindow>,
}
impl<'w> Frame<'w> {
pub fn surface_view(&self) -> &wgpu::TextureView {
self.surface
.as_ref()
.expect("Surface already consumed")
.view()
}
pub fn try_surface_view(&self) -> Option<&wgpu::TextureView> {
self.surface.as_ref().map(|s| s.view())
}
pub fn depth_view(&self) -> Option<&wgpu::TextureView> {
self.window.depth_view_ref()
}
pub fn surface_format(&self) -> wgpu::TextureFormat {
self.surface_format
}
pub fn size(&self) -> (u32, u32) {
self.window.size()
}
pub fn graphics(&self) -> &GraphicsContext {
self.window.graphics()
}
pub fn device(&self) -> &wgpu::Device {
self.window.graphics().device()
}
pub fn queue(&self) -> &wgpu::Queue {
self.window.graphics().queue()
}
pub fn stats(&self) -> FrameStats {
self.stats.to_frame_stats()
}
pub fn gpu_profiler(&self) -> Option<&GpuFrameProfiler> {
self.gpu_profiler.as_deref()
}
pub fn has_gpu_profiler(&self) -> bool {
self.gpu_profiler.is_some()
}
pub fn create_encoder(&self, label: Option<&str>) -> wgpu::CommandEncoder {
self.device()
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label })
}
pub fn add_command_buffer(&self, buffer: wgpu::CommandBuffer) {
self.command_buffers.borrow_mut().push(buffer);
}
pub fn render_pass(&self) -> RenderPassBuilder<'_, 'w> {
RenderPassBuilder::new(self)
}
pub fn compute_pass(&self) -> crate::compute::ComputePassBuilder<'_, 'w> {
crate::compute::ComputePassBuilder::new(self)
}
pub fn submit(self) {
drop(self);
}
fn submit_inner(&self) {
profile_function!();
if self.stats.passes() == 0 {
tracing::warn!("No render passes were executed for this frame");
}
if let Some(ref profiler) = self.gpu_profiler {
let mut resolve_encoder = self.create_encoder(Some("Profiler Resolve"));
profiler.resolve_queries(&mut resolve_encoder);
self.command_buffers
.borrow_mut()
.push(resolve_encoder.finish());
}
let buffers = std::mem::take(&mut *self.command_buffers.borrow_mut());
if !buffers.is_empty() {
profile_scope!("submit_commands");
self.queue().submit(buffers);
}
if let Some(_surface) = self.surface.as_ref() {
profile_scope!("present_surface");
}
if let Some(ref profiler) = self.gpu_profiler
&& let Err(e) = profiler.end_frame()
{
tracing::warn!("GPU profiler end_frame error: {e:?}");
}
}
}
impl Drop for Frame<'_> {
fn drop(&mut self) {
if !self.submitted.get() {
self.submitted.set(true);
self.submit_inner();
}
if let Some(surface) = self.surface.take() {
profile_scope!("present_surface");
surface.texture.present();
}
self.winit_window.request_redraw();
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum ColorTarget<'a> {
#[default]
Surface,
Custom(&'a wgpu::TextureView),
Framebuffer(&'a Framebuffer),
}
#[derive(Debug, Clone, Copy, Default)]
pub enum ColorOp {
Clear(wgpu::Color),
#[default]
Load,
}
impl From<Color> for ColorOp {
fn from(color: Color) -> Self {
Self::Clear(color.to_wgpu())
}
}
impl From<wgpu::Color> for ColorOp {
fn from(color: wgpu::Color) -> Self {
Self::Clear(color)
}
}
#[derive(Debug, Clone, Copy)]
pub enum DepthOp {
Clear(f32),
Load,
ReadOnly,
}
impl Default for DepthOp {
fn default() -> Self {
Self::Clear(1.0)
}
}
pub struct RenderPassBuilder<'f, 'w> {
frame: &'f Frame<'w>,
color_target: ColorTarget<'f>,
color_op: ColorOp,
depth_view: Option<&'f wgpu::TextureView>,
depth_op: DepthOp,
label: Option<String>,
}
impl<'f, 'w> RenderPassBuilder<'f, 'w> {
pub(crate) fn new(frame: &'f Frame<'w>) -> Self {
Self {
frame,
color_target: ColorTarget::Surface,
color_op: ColorOp::Load,
depth_view: None,
depth_op: DepthOp::default(),
label: None,
}
}
pub fn target(mut self, target: RenderTarget<'f>) -> Self {
match target {
RenderTarget::Surface => {
self.color_target = ColorTarget::Surface;
}
RenderTarget::SurfaceWithDepth {
depth_view,
clear_value,
} => {
self.color_target = ColorTarget::Surface;
self.depth_view = Some(depth_view);
if let Some(v) = clear_value {
self.depth_op = DepthOp::Clear(v);
} else {
self.depth_op = DepthOp::Load;
}
}
RenderTarget::Framebuffer(fb) => {
self.color_target = ColorTarget::Framebuffer(fb);
if let Some(dv) = fb.depth_view() {
self.depth_view = Some(dv);
}
}
}
self
}
pub fn to_surface(mut self) -> Self {
self.color_target = ColorTarget::Surface;
self
}
pub fn to_framebuffer(mut self, fb: &'f Framebuffer) -> Self {
self.color_target = ColorTarget::Framebuffer(fb);
if let Some(dv) = fb.depth_view() {
self.depth_view = Some(dv);
}
self
}
pub fn to_texture(mut self, view: &'f wgpu::TextureView) -> Self {
self.color_target = ColorTarget::Custom(view);
self
}
pub fn clear_color(mut self, color: impl Into<ColorOp>) -> Self {
self.color_op = color.into();
self
}
pub fn load_color(mut self) -> Self {
self.color_op = ColorOp::Load;
self
}
pub fn depth_attachment(mut self, view: &'f wgpu::TextureView) -> Self {
self.depth_view = Some(view);
self
}
pub fn with_window_depth(mut self) -> Self {
self.depth_view = Some(
self.frame
.depth_view()
.expect("Window must have depth buffer for with_window_depth()"),
);
self
}
pub fn with_window_depth_if_available(mut self) -> Self {
if let Some(dv) = self.frame.depth_view() {
self.depth_view = Some(dv);
}
self
}
pub fn clear_depth(mut self, value: f32) -> Self {
self.depth_op = DepthOp::Clear(value);
self
}
pub fn load_depth(mut self) -> Self {
self.depth_op = DepthOp::Load;
self
}
pub fn depth_readonly(mut self) -> Self {
self.depth_op = DepthOp::ReadOnly;
self
}
pub fn label(mut self, name: impl Into<String>) -> Self {
self.label = Some(name.into());
self
}
pub fn build(self) -> RenderPass<'f> {
profile_function!();
let label = self.label.clone();
let label_str = label.as_deref();
let encoder = self
.frame
.device()
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: label_str });
let color_view = match self.color_target {
ColorTarget::Surface => self.frame.surface_view(),
ColorTarget::Custom(v) => v,
ColorTarget::Framebuffer(fb) => fb.render_view(),
};
let color_ops = match self.color_op {
ColorOp::Clear(color) => wgpu::Operations {
load: wgpu::LoadOp::Clear(color),
store: wgpu::StoreOp::Store,
},
ColorOp::Load => wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
};
let resolve_target = match self.color_target {
ColorTarget::Framebuffer(fb) => fb.resolve_target(),
_ => None,
};
let color_attachments = [Some(wgpu::RenderPassColorAttachment {
view: color_view,
resolve_target,
ops: color_ops,
depth_slice: None,
})];
let depth_attachment = self.depth_view.map(|view| {
let (depth_ops, read_only) = match self.depth_op {
DepthOp::Clear(value) => (
Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(value),
store: wgpu::StoreOp::Store,
}),
false,
),
DepthOp::Load => (
Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
false,
),
DepthOp::ReadOnly => (
Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Discard,
}),
true,
),
};
wgpu::RenderPassDepthStencilAttachment {
view,
depth_ops: if read_only { None } else { depth_ops },
stencil_ops: None,
}
});
self.frame.stats.increment_passes();
let mut encoder = encoder;
let pass = encoder
.begin_render_pass(&wgpu::RenderPassDescriptor {
label: label_str,
color_attachments: &color_attachments,
depth_stencil_attachment: depth_attachment,
timestamp_writes: None,
occlusion_query_set: None,
})
.forget_lifetime();
RenderPass {
frame: self.frame,
encoder: Some(encoder),
pass: Some(pass),
stats: self.frame.stats.clone(),
#[cfg(feature = "gpu-profiling")]
profiler_scope: None,
}
}
}
pub struct RenderPass<'f> {
frame: &'f Frame<'f>,
encoder: Option<wgpu::CommandEncoder>,
pass: Option<wgpu::RenderPass<'static>>,
stats: Arc<AtomicFrameStats>,
#[cfg(feature = "gpu-profiling")]
profiler_scope: Option<wgpu_profiler::scope::OwningScope>,
}
impl<'f> RenderPass<'f> {
pub fn wgpu_pass(&mut self) -> &mut wgpu::RenderPass<'static> {
self.pass.as_mut().expect("RenderPass already consumed")
}
pub fn wgpu_pass_ref(&self) -> &wgpu::RenderPass<'static> {
self.pass.as_ref().expect("RenderPass already consumed")
}
pub fn try_wgpu_pass(&mut self) -> Option<&mut wgpu::RenderPass<'static>> {
self.pass.as_mut()
}
pub fn is_valid(&self) -> bool {
self.pass.is_some()
}
pub fn raw_pass(&mut self) -> &mut wgpu::RenderPass<'static> {
self.wgpu_pass()
}
pub fn encoder(&self) -> Option<&wgpu::CommandEncoder> {
self.encoder.as_ref()
}
pub fn encoder_mut(&mut self) -> Option<&mut wgpu::CommandEncoder> {
self.encoder.as_mut()
}
pub fn graphics(&self) -> &GraphicsContext {
self.frame.graphics()
}
pub fn record_draw_call(&self) {
self.stats.increment_draw_calls();
}
pub fn into_encoder(mut self) -> wgpu::CommandEncoder {
drop(self.pass.take());
self.encoder.take().expect("Encoder already taken")
}
pub fn finish(self) {
drop(self);
}
pub fn set_viewport_physical(
&mut self,
rect: astrelis_core::geometry::PhysicalRect<f32>,
min_depth: f32,
max_depth: f32,
) {
self.wgpu_pass().set_viewport(
rect.x,
rect.y,
rect.width,
rect.height,
min_depth,
max_depth,
);
}
pub fn set_viewport_logical(
&mut self,
rect: astrelis_core::geometry::LogicalRect<f32>,
min_depth: f32,
max_depth: f32,
scale: astrelis_core::geometry::ScaleFactor,
) {
let physical = rect.to_physical_f32(scale);
self.set_viewport_physical(physical, min_depth, max_depth);
}
pub fn set_viewport(&mut self, viewport: &crate::Viewport) {
self.wgpu_pass().set_viewport(
viewport.position.x,
viewport.position.y,
viewport.size.width,
viewport.size.height,
0.0,
1.0,
);
}
pub fn set_scissor_physical(&mut self, rect: astrelis_core::geometry::PhysicalRect<u32>) {
self.wgpu_pass()
.set_scissor_rect(rect.x, rect.y, rect.width, rect.height);
}
pub fn set_scissor_logical(
&mut self,
rect: astrelis_core::geometry::LogicalRect<f32>,
scale: astrelis_core::geometry::ScaleFactor,
) {
let physical = rect.to_physical(scale);
self.set_scissor_physical(physical);
}
pub fn set_pipeline(&mut self, pipeline: &wgpu::RenderPipeline) {
self.wgpu_pass().set_pipeline(pipeline);
}
pub fn set_bind_group(&mut self, index: u32, bind_group: &wgpu::BindGroup, offsets: &[u32]) {
self.wgpu_pass().set_bind_group(index, bind_group, offsets);
}
pub fn set_vertex_buffer(&mut self, slot: u32, buffer_slice: wgpu::BufferSlice<'_>) {
self.wgpu_pass().set_vertex_buffer(slot, buffer_slice);
}
pub fn set_index_buffer(
&mut self,
buffer_slice: wgpu::BufferSlice<'_>,
format: wgpu::IndexFormat,
) {
self.wgpu_pass().set_index_buffer(buffer_slice, format);
}
pub fn draw(&mut self, vertices: std::ops::Range<u32>, instances: std::ops::Range<u32>) {
self.wgpu_pass().draw(vertices, instances);
self.stats.increment_draw_calls();
}
pub fn draw_indexed(
&mut self,
indices: std::ops::Range<u32>,
base_vertex: i32,
instances: std::ops::Range<u32>,
) {
self.wgpu_pass()
.draw_indexed(indices, base_vertex, instances);
self.stats.increment_draw_calls();
}
pub fn insert_debug_marker(&mut self, label: &str) {
self.wgpu_pass().insert_debug_marker(label);
}
pub fn push_debug_group(&mut self, label: &str) {
self.wgpu_pass().push_debug_group(label);
}
pub fn pop_debug_group(&mut self) {
self.wgpu_pass().pop_debug_group();
}
pub fn set_push_constants<T: bytemuck::Pod>(
&mut self,
stages: wgpu::ShaderStages,
offset: u32,
data: &T,
) {
self.wgpu_pass()
.set_push_constants(stages, offset, bytemuck::bytes_of(data));
}
pub fn set_push_constants_raw(&mut self, stages: wgpu::ShaderStages, offset: u32, data: &[u8]) {
self.wgpu_pass().set_push_constants(stages, offset, data);
}
}
impl Drop for RenderPass<'_> {
fn drop(&mut self) {
profile_function!();
#[cfg(feature = "gpu-profiling")]
drop(self.profiler_scope.take());
drop(self.pass.take());
if let Some(encoder) = self.encoder.take() {
let command_buffer = encoder.finish();
self.frame.command_buffers.borrow_mut().push(command_buffer);
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum ClearOp {
#[default]
Load,
Clear(wgpu::Color),
}
impl From<wgpu::Color> for ClearOp {
fn from(color: wgpu::Color) -> Self {
ClearOp::Clear(color)
}
}
impl From<Color> for ClearOp {
fn from(color: Color) -> Self {
ClearOp::Clear(color.to_wgpu())
}
}
#[derive(Debug, Clone, Copy)]
pub enum DepthClearOp {
Load,
Clear(f32),
}
impl Default for DepthClearOp {
fn default() -> Self {
DepthClearOp::Clear(1.0)
}
}