From 7756f222e0f51e40ca1da69d25d9826fefe2fd3d Mon Sep 17 00:00:00 2001 From: Fedor Logachev Date: Sun, 25 Aug 2024 18:54:56 -0600 Subject: [PATCH] graphics: Implement MSAA render textures Gleaned implementation idea directly from sokol. For now only for OpenGL. --- examples/msaa_render_texture.rs | 410 ++++++++++++++++++++++++++++++++ src/graphics.rs | 15 +- src/graphics/gl.rs | 136 +++++++++-- src/graphics/gl/cache.rs | 1 + src/graphics/metal.rs | 4 + 5 files changed, 544 insertions(+), 22 deletions(-) create mode 100644 examples/msaa_render_texture.rs diff --git a/examples/msaa_render_texture.rs b/examples/msaa_render_texture.rs new file mode 100644 index 00000000..b99f5d31 --- /dev/null +++ b/examples/msaa_render_texture.rs @@ -0,0 +1,410 @@ +use miniquad::*; + +use glam::{vec3, Mat4}; + +struct Stage { + display_pipeline: Pipeline, + display_bind: Bindings, + offscreen_pipeline: Pipeline, + offscreen_bind: Bindings, + offscreen_pass: RenderPass, + rx: f32, + ry: f32, + ctx: Box, +} + +impl Stage { + pub fn new() -> Stage { + let mut ctx: Box = window::new_rendering_backend(); + + let color_img = ctx.new_render_texture(TextureParams { + width: 256, + height: 256, + format: TextureFormat::RGBA8, + sample_count: 4, + ..Default::default() + }); + let color_resolve_img = ctx.new_render_texture(TextureParams { + width: 256, + height: 256, + format: TextureFormat::RGBA8, + ..Default::default() + }); + let depth_img = ctx.new_render_texture(TextureParams { + width: 256, + height: 256, + format: TextureFormat::Depth, + sample_count: 4, + ..Default::default() + }); + + let offscreen_pass = + ctx.new_render_pass_mrt(&[color_img], Some(&[color_resolve_img]), Some(depth_img)); + + #[rustfmt::skip] + let vertices: &[f32] = &[ + /* pos color uvs */ + -1.0, -1.0, -1.0, 1.0, 0.5, 0.5, 1.0, 0.0, 0.0, + 1.0, -1.0, -1.0, 1.0, 0.5, 0.5, 1.0, 1.0, 0.0, + 1.0, 1.0, -1.0, 1.0, 0.5, 0.5, 1.0, 1.0, 1.0, + -1.0, 1.0, -1.0, 1.0, 0.5, 0.5, 1.0, 0.0, 1.0, + + -1.0, -1.0, 1.0, 0.5, 1.0, 0.5, 1.0, 0.0, 0.0, + 1.0, -1.0, 1.0, 0.5, 1.0, 0.5, 1.0, 1.0, 0.0, + 1.0, 1.0, 1.0, 0.5, 1.0, 0.5, 1.0, 1.0, 1.0, + -1.0, 1.0, 1.0, 0.5, 1.0, 0.5, 1.0, 0.0, 1.0, + + -1.0, -1.0, -1.0, 0.5, 0.5, 1.0, 1.0, 0.0, 0.0, + -1.0, 1.0, -1.0, 0.5, 0.5, 1.0, 1.0, 1.0, 0.0, + -1.0, 1.0, 1.0, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0, + -1.0, -1.0, 1.0, 0.5, 0.5, 1.0, 1.0, 0.0, 1.0, + + 1.0, -1.0, -1.0, 1.0, 0.5, 0.0, 1.0, 0.0, 0.0, + 1.0, 1.0, -1.0, 1.0, 0.5, 0.0, 1.0, 1.0, 0.0, + 1.0, 1.0, 1.0, 1.0, 0.5, 0.0, 1.0, 1.0, 1.0, + 1.0, -1.0, 1.0, 1.0, 0.5, 0.0, 1.0, 0.0, 1.0, + + -1.0, -1.0, -1.0, 0.0, 0.5, 1.0, 1.0, 0.0, 0.0, + -1.0, -1.0, 1.0, 0.0, 0.5, 1.0, 1.0, 1.0, 0.0, + 1.0, -1.0, 1.0, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0, + 1.0, -1.0, -1.0, 0.0, 0.5, 1.0, 1.0, 0.0, 1.0, + + -1.0, 1.0, -1.0, 1.0, 0.0, 0.5, 1.0, 0.0, 0.0, + -1.0, 1.0, 1.0, 1.0, 0.0, 0.5, 1.0, 1.0, 0.0, + 1.0, 1.0, 1.0, 1.0, 0.0, 0.5, 1.0, 1.0, 1.0, + 1.0, 1.0, -1.0, 1.0, 0.0, 0.5, 1.0, 0.0, 1.0 + ]; + + let vertex_buffer = ctx.new_buffer( + BufferType::VertexBuffer, + BufferUsage::Immutable, + BufferSource::slice(&vertices), + ); + + #[rustfmt::skip] + let indices: &[u16] = &[ + 0, 1, 2, 0, 2, 3, + 6, 5, 4, 7, 6, 4, + 8, 9, 10, 8, 10, 11, + 14, 13, 12, 15, 14, 12, + 16, 17, 18, 16, 18, 19, + 22, 21, 20, 23, 22, 20 + ]; + + let index_buffer = ctx.new_buffer( + BufferType::IndexBuffer, + BufferUsage::Immutable, + BufferSource::slice(&indices), + ); + + let offscreen_bind = Bindings { + vertex_buffers: vec![vertex_buffer.clone()], + index_buffer: index_buffer.clone(), + images: vec![], + }; + + let display_bind = { + #[rustfmt::skip] + let vertices: &[f32] = &[ + -0.5, 0.0, -0.5, 1.0, 1.0, 1.0, 1.0, 0., 0., + 0.5, 0.0, -0.5, 1.0, 1.0, 1.0, 1.0, 1., 0., + 0.5, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0, 1., 1., + -0.5, 0.0, 0.5, 1.0, 1.0, 1.0, 1.0, 0., 1., + ]; + let vertex_buffer = ctx.new_buffer( + BufferType::VertexBuffer, + BufferUsage::Immutable, + BufferSource::slice(&vertices), + ); + let indices: [u16; 6] = [0, 1, 2, 0, 2, 3]; + let index_buffer = ctx.new_buffer( + BufferType::IndexBuffer, + BufferUsage::Immutable, + BufferSource::slice(&indices), + ); + Bindings { + vertex_buffers: vec![vertex_buffer], + index_buffer: index_buffer, + images: vec![color_resolve_img], + } + }; + + let source = match ctx.info().backend { + Backend::OpenGl => ShaderSource::Glsl { + vertex: display_shader::VERTEX, + fragment: display_shader::FRAGMENT, + }, + Backend::Metal => ShaderSource::Msl { + program: display_shader::METAL, + }, + }; + let default_shader = ctx.new_shader(source, display_shader::meta()).unwrap(); + + let display_pipeline = ctx.new_pipeline( + &[BufferLayout::default()], + &[ + VertexAttribute::new("in_pos", VertexFormat::Float3), + VertexAttribute::new("in_color", VertexFormat::Float4), + VertexAttribute::new("in_uv", VertexFormat::Float2), + ], + default_shader, + PipelineParams { + depth_test: Comparison::LessOrEqual, + depth_write: true, + ..Default::default() + }, + ); + + let source = match ctx.info().backend { + Backend::OpenGl => ShaderSource::Glsl { + vertex: offscreen_shader::VERTEX, + fragment: offscreen_shader::FRAGMENT, + }, + Backend::Metal => ShaderSource::Msl { + program: offscreen_shader::METAL, + }, + }; + let offscreen_shader = ctx.new_shader(source, offscreen_shader::meta()).unwrap(); + + let offscreen_pipeline = ctx.new_pipeline( + &[BufferLayout { + stride: 36, + ..Default::default() + }], + &[ + VertexAttribute::new("in_pos", VertexFormat::Float3), + VertexAttribute::new("in_color", VertexFormat::Float4), + ], + offscreen_shader, + PipelineParams { + depth_test: Comparison::LessOrEqual, + depth_write: true, + ..Default::default() + }, + ); + + Stage { + display_pipeline, + display_bind, + offscreen_pipeline, + offscreen_bind, + offscreen_pass, + rx: 0., + ry: 0., + ctx, + } + } +} + +impl EventHandler for Stage { + fn update(&mut self) {} + + fn draw(&mut self) { + let (width, height) = window::screen_size(); + let proj = Mat4::perspective_rh_gl(60.0f32.to_radians(), width / height, 0.01, 10.0); + let view = Mat4::look_at_rh( + vec3(0.0, 1.5, 3.0), + vec3(0.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + ); + let view_proj = proj * view; + + self.rx += 0.01; + self.ry += 0.03; + let model = Mat4::from_rotation_y(self.ry) * Mat4::from_rotation_x(self.rx); + + let vs_params = display_shader::Uniforms { + mvp: view_proj * model, + }; + + // the offscreen pass, rendering a rotating, untextured cube into a render target image + self.ctx.begin_pass( + Some(self.offscreen_pass), + PassAction::clear_color(1.0, 1.0, 1.0, 1.0), + ); + self.ctx.apply_pipeline(&self.offscreen_pipeline); + self.ctx.apply_bindings(&self.offscreen_bind); + self.ctx.apply_uniforms(UniformsSource::table(&vs_params)); + self.ctx.draw(0, 36, 1); + self.ctx.end_render_pass(); + + // and the display-pass, rendering a rotating, textured cube, using the + // previously rendered offscreen render-target as texture + self.ctx + .begin_default_pass(PassAction::clear_color(0.0, 0., 0.45, 1.)); + self.ctx.apply_pipeline(&self.display_pipeline); + self.ctx.apply_bindings(&self.display_bind); + self.ctx.apply_uniforms(UniformsSource::table(&vs_params)); + self.ctx.draw(0, 36, 1); + self.ctx.end_render_pass(); + + self.ctx.commit_frame(); + } +} + +fn main() { + let mut conf = conf::Conf::default(); + let metal = std::env::args().nth(1).as_deref() == Some("metal"); + conf.platform.apple_gfx_api = if metal { + conf::AppleGfxApi::Metal + } else { + conf::AppleGfxApi::OpenGl + }; + + miniquad::start(conf, move || Box::new(Stage::new())); +} + +mod display_shader { + use miniquad::*; + + pub const VERTEX: &str = r#"#version 100 + attribute vec4 in_pos; + attribute vec4 in_color; + attribute vec2 in_uv; + + varying lowp vec4 color; + varying lowp vec2 uv; + + uniform mat4 mvp; + + void main() { + gl_Position = vec4(in_pos.x * 2., in_pos.z * 2., 0.0, 1.0); + color = in_color; + uv = in_uv; + } + "#; + + pub const FRAGMENT: &str = r#"#version 100 + varying lowp vec4 color; + varying lowp vec2 uv; + + uniform sampler2D tex; + + void main() { + gl_FragColor = color * texture2D(tex, uv); + } + "#; + + pub const METAL: &str = r#"#include + using namespace metal; + + struct Uniforms + { + float4x4 mvp; + }; + + struct Vertex + { + float3 in_pos [[attribute(0)]]; + float4 in_color [[attribute(1)]]; + float2 in_uv [[attribute(2)]]; + }; + + struct RasterizerData + { + float4 position [[position]]; + float4 color [[user(locn0)]]; + float2 uv [[user(locn1)]]; + }; + + vertex RasterizerData vertexShader(Vertex v [[stage_in]], constant Uniforms& uniforms [[buffer(0)]]) + { + RasterizerData out; + + out.position = uniforms.mvp * float4(v.in_pos, 1.0); + out.color = v.in_color; + out.uv = v.in_uv; + + return out; + } + + fragment float4 fragmentShader(RasterizerData in [[stage_in]], texture2d tex [[texture(0)]], sampler texSmplr [[sampler(0)]]) + { + return in.color * tex.sample(texSmplr, in.uv); + }"#; + + pub fn meta() -> ShaderMeta { + ShaderMeta { + images: vec!["tex".to_string()], + uniforms: UniformBlockLayout { + uniforms: vec![UniformDesc::new("mvp", UniformType::Mat4)], + }, + } + } + + #[repr(C)] + pub struct Uniforms { + pub mvp: glam::Mat4, + } +} + +mod offscreen_shader { + use miniquad::*; + + pub const VERTEX: &str = r#"#version 100 + attribute vec3 in_pos; + attribute vec4 in_color; + + varying lowp vec4 color; + + uniform mat4 mvp; + + void main() { + gl_Position = mvp * vec4(in_pos, 1.0); + color = in_color; + } + "#; + + pub const FRAGMENT: &str = r#"#version 100 + varying lowp vec4 color; + + void main() { + gl_FragColor = color; + } + "#; + + pub const METAL: &str = r#"#include + + using namespace metal; + + struct Uniforms + { + float4x4 mvp; + }; + + struct Vertex + { + float3 in_pos [[attribute(0)]]; + float4 in_color [[attribute(1)]]; + }; + + struct RasterizerData + { + float4 position [[position]]; + float4 color [[user(locn0)]]; + }; + + vertex RasterizerData vertexShader(Vertex v [[stage_in]], constant Uniforms& uniforms [[buffer(0)]]) + { + RasterizerData out; + + out.position = uniforms.mvp * float4(v.in_pos, 1.0) * float4(1.0, -1.0, 1.0, 1.0); + out.color = v.in_color; + + return out; + } + + fragment float4 fragmentShader(RasterizerData in [[stage_in]]) + { + return in.color; + }"#; + + pub fn meta() -> ShaderMeta { + ShaderMeta { + images: vec![], + uniforms: UniformBlockLayout { + uniforms: vec![UniformDesc::new("mvp", UniformType::Mat4)], + }, + } + } +} diff --git a/src/graphics.rs b/src/graphics.rs index 138dd207..28f9e304 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -387,6 +387,13 @@ pub struct TextureParams { // And reallocate non-mipmapped texture(on metal) on generateMipmaps call // But! Reallocating cubemaps is too much struggle, so leave it for later. pub allocate_mipmaps: bool, + /// Only used for render textures. `sample_count > 1` allows anti-aliased render textures. + /// + /// On OpenGL, for a `sample_count > 1` render texture, render buffer object will + /// be created instead of a regulat texture. + /// + /// The only way to use + pub sample_count: i32, } impl Default for TextureParams { @@ -401,6 +408,7 @@ impl Default for TextureParams { width: 0, height: 0, allocate_mipmaps: false, + sample_count: 0, } } } @@ -1132,6 +1140,7 @@ pub trait RenderingBackend { mag_filter: FilterMode::Linear, mipmap_filter: MipmapFilterMode::None, allocate_mipmaps: false, + sample_count: 0, }, ) } @@ -1189,12 +1198,16 @@ pub trait RenderingBackend { color_img: TextureId, depth_img: Option, ) -> RenderPass { - self.new_render_pass_mrt(&[color_img], depth_img) + self.new_render_pass_mrt(&[color_img], None, depth_img) } /// Same as "new_render_pass", but allows multiple color attachments. + /// if `resolve_img` is set, MSAA-resolve operation will happen in `end_render_pass` + /// this operation require `color_img` to have sample_count > 1,resolve_img have + /// sample_count == 1, and color_img.len() should be equal to resolve_img.len() fn new_render_pass_mrt( &mut self, color_img: &[TextureId], + resolve_img: Option<&[TextureId]>, depth_img: Option, ) -> RenderPass; /// panics for depth-only or multiple color attachment render pass diff --git a/src/graphics/gl.rs b/src/graphics/gl.rs index 538ba1e4..41a8daaa 100644 --- a/src/graphics/gl.rs +++ b/src/graphics/gl.rs @@ -137,7 +137,7 @@ impl From for GLenum { impl Texture { pub fn new( ctx: &mut GlContext, - _access: TextureAccess, + access: TextureAccess, source: TextureSource, params: TextureParams, ) -> Texture { @@ -147,9 +147,33 @@ impl Texture { bytes_data.len() ); } - + if access != TextureAccess::RenderTarget { + assert!( + params.sample_count == 0, + "Multisampling is only supported for render textures" + ); + } let (internal_format, format, pixel_type) = params.format.into(); + if access == TextureAccess::RenderTarget && params.sample_count != 0 { + let mut renderbuffer: u32 = 0; + unsafe { + glGenRenderbuffers(1, &mut renderbuffer as *mut _); + glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer as _); + glRenderbufferStorageMultisample( + GL_RENDERBUFFER, + params.sample_count, + internal_format, + params.width as _, + params.height as _, + ); + } + return Texture { + raw: renderbuffer, + params, + }; + } + ctx.cache.store_texture_binding(0); let mut texture: GLuint = 0; @@ -419,6 +443,7 @@ fn get_uniform_location(program: GLuint, name: &str) -> Option { pub(crate) struct RenderPassInternal { gl_fb: GLuint, color_textures: Vec, + resolves: Option>, depth_texture: Option, } @@ -444,7 +469,6 @@ pub struct GlContext { textures: Textures, default_framebuffer: GLuint, pub(crate) cache: GlCache, - pub(crate) info: ContextInfo, } @@ -481,6 +505,7 @@ impl GlContext { index_type: None, vertex_buffer: 0, cur_pipeline: None, + cur_pass: None, color_blend: None, alpha_blend: None, stencil: None, @@ -950,6 +975,7 @@ impl RenderingBackend for GlContext { fn new_render_pass_mrt( &mut self, color_img: &[TextureId], + resolve_img: Option<&[TextureId]>, depth_img: Option, ) -> RenderPass { if color_img.is_empty() && depth_img.is_none() { @@ -957,40 +983,82 @@ impl RenderingBackend for GlContext { } let mut gl_fb = 0; + let mut resolves = vec![]; unsafe { glGenFramebuffers(1, &mut gl_fb as *mut _); glBindFramebuffer(GL_FRAMEBUFFER, gl_fb); for (i, color_img) in color_img.iter().enumerate() { - glFramebufferTexture2D( - GL_FRAMEBUFFER, - GL_COLOR_ATTACHMENT0 + i as u32, - GL_TEXTURE_2D, - self.textures.get(*color_img).raw, - 0, - ); + let texture = self.textures.get(*color_img); + if texture.params.sample_count != 0 { + glFramebufferRenderbuffer( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0 + i as u32, + GL_RENDERBUFFER, + texture.raw, + ); + } else { + glFramebufferTexture2D( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0 + i as u32, + GL_TEXTURE_2D, + texture.raw, + 0, + ); + } } if let Some(depth_img) = depth_img { - glFramebufferTexture2D( - GL_FRAMEBUFFER, - GL_DEPTH_ATTACHMENT, - GL_TEXTURE_2D, - self.textures.get(depth_img).raw, - 0, - ); + let texture = self.textures.get(depth_img); + if texture.params.sample_count != 0 { + glFramebufferRenderbuffer( + GL_FRAMEBUFFER, + GL_DEPTH_ATTACHMENT, + GL_RENDERBUFFER, + texture.raw, + ); + } else { + glFramebufferTexture2D( + GL_FRAMEBUFFER, + GL_DEPTH_ATTACHMENT, + GL_TEXTURE_2D, + texture.raw, + 0, + ); + } + } + let mut attachments = vec![]; + for i in 0..color_img.len() { + attachments.push(GL_COLOR_ATTACHMENT0 + i as u32); } + if color_img.len() > 1 { - let mut attachments = vec![]; - for i in 0..color_img.len() { - attachments.push(GL_COLOR_ATTACHMENT0 + i as u32); - } glDrawBuffers(color_img.len() as _, attachments.as_ptr() as _); } + if let Some(resolve_img) = resolve_img { + for (i, resolve_img) in resolve_img.iter().enumerate() { + let mut resolve_fb = 0; + glGenFramebuffers(1, &mut resolve_fb as *mut _); + glBindFramebuffer(GL_FRAMEBUFFER, resolve_fb); + resolves.push((resolve_fb, *resolve_img)); + let texture = self.textures.get(*resolve_img); + glFramebufferTexture2D( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0 + i as u32, + GL_TEXTURE_2D, + texture.raw, + 0, + ); + let fb_status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + assert!(fb_status != 0); + glDrawBuffers(1, attachments.as_ptr() as _); + } + } glBindFramebuffer(GL_FRAMEBUFFER, self.default_framebuffer); } let pass = RenderPassInternal { gl_fb, color_textures: color_img.to_vec(), + resolves: Some(resolves), depth_texture: depth_img, }; @@ -1467,6 +1535,7 @@ impl RenderingBackend for GlContext { } fn begin_pass(&mut self, pass: Option, action: PassAction) { + self.cache.cur_pass = pass; let (framebuffer, w, h) = match pass { None => { let (screen_width, screen_height) = window::screen_size(); @@ -1513,6 +1582,31 @@ impl RenderingBackend for GlContext { fn end_render_pass(&mut self) { unsafe { + if let Some(pass) = self.cache.cur_pass.take() { + let pass = &self.passes[pass.0]; + if let Some(resolves) = &pass.resolves { + glBindFramebuffer(GL_READ_FRAMEBUFFER, pass.gl_fb); + for (i, (resolve_fb, resolve_img)) in resolves.iter().enumerate() { + let texture = self.textures.get(*resolve_img); + let w = texture.params.width; + let h = texture.params.height; + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, *resolve_fb); + glReadBuffer(GL_COLOR_ATTACHMENT0 + i as u32); + glBlitFramebuffer( + 0, + 0, + w as _, + h as _, + 0, + 0, + w as _, + h as _, + GL_COLOR_BUFFER_BIT, + GL_NEAREST, + ); + } + } + } glBindFramebuffer(GL_FRAMEBUFFER, self.default_framebuffer); self.cache.bind_buffer(GL_ARRAY_BUFFER, 0, None); self.cache.bind_buffer(GL_ELEMENT_ARRAY_BUFFER, 0, None); diff --git a/src/graphics/gl/cache.rs b/src/graphics/gl/cache.rs index 3e252d92..ca194d00 100644 --- a/src/graphics/gl/cache.rs +++ b/src/graphics/gl/cache.rs @@ -36,6 +36,7 @@ pub struct GlCache { pub vertex_buffer: GLuint, pub textures: [CachedTexture; MAX_SHADERSTAGE_IMAGES], pub cur_pipeline: Option, + pub cur_pass: Option, pub color_blend: Option, pub alpha_blend: Option, pub stencil: Option, diff --git a/src/graphics/metal.rs b/src/graphics/metal.rs index a2ab5b03..69741be5 100644 --- a/src/graphics/metal.rs +++ b/src/graphics/metal.rs @@ -527,8 +527,12 @@ impl RenderingBackend for MetalContext { fn new_render_pass_mrt( &mut self, color_img: &[TextureId], + resolve_img: Option<&[TextureId]>, depth_img: Option, ) -> RenderPass { + if resolve_img.is_some() { + unimplemented!("resolve textures are not yet implemented on metal"); + } unsafe { let render_pass_desc = msg_send_![class!(MTLRenderPassDescriptor), renderPassDescriptor];