diff --git a/src/graphics.rs b/src/graphics.rs index dbdfffc65..8efaec6b7 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -400,6 +400,18 @@ pub struct TextureParams { /// be created instead of a regulat texture. /// pub sample_count: i32, + /// Only used for render textures. A transient render texture's contents + /// last only as long as the pass drawing into it: a pass that doesn't + /// clear it starts with them undefined, and only what a multisampled one + /// resolves into outlives the pass. + /// + /// On Metal, a GPU with tile memory keeps them there alone, so the + /// texture takes up no memory, and no pass stores them. On OpenGL ES with + /// [`Features::multisampled_render_to_texture`], a pass resolving a + /// multisampled one keeps its samples in tile memory, and a pass that + /// doesn't clear one doesn't load it. Elsewhere, OpenGL keeps them as it + /// does any texture's. + pub transient: bool, } impl Default for TextureParams { @@ -415,6 +427,7 @@ impl Default for TextureParams { height: 0, allocate_mipmaps: false, sample_count: 1, + transient: false, } } } @@ -582,6 +595,13 @@ pub struct Features { /// With resolve_attachments: false, not-none resolve_img in new_render_pass will /// result in a runtime panic. pub resolve_attachments: bool, + /// Whether the context is OpenGL ES 3 with + /// `GL_EXT_multisampled_render_to_texture`. A pass that resolves a + /// [transient](TextureParams::transient) multisampled color texture + /// then draws into its resolve texture, keeping the samples in tile + /// memory and resolving them as the GPU writes the tile out, rather + /// than copying them at every `end_render_pass`. Always false on Metal. + pub multisampled_render_to_texture: bool, } impl Default for Features { @@ -589,6 +609,7 @@ impl Default for Features { Features { instancing: true, resolve_attachments: true, + multisampled_render_to_texture: false, } } } @@ -1160,6 +1181,7 @@ pub trait RenderingBackend { mipmap_filter: MipmapFilterMode::None, allocate_mipmaps: false, sample_count: 1, + transient: false, }, ) } diff --git a/src/graphics/gl.rs b/src/graphics/gl.rs index 5fb34c096..dc875143b 100644 --- a/src/graphics/gl.rs +++ b/src/graphics/gl.rs @@ -7,6 +7,46 @@ mod cache; use super::*; use cache::*; +#[cfg(target_arch = "wasm32")] +use webgl_stand_ins::{ + glFramebufferTexture2DMultisampleEXT, glInvalidateFramebuffer, + glRenderbufferStorageMultisampleEXT, +}; + +/// WebGL has no `GL_EXT_multisampled_render_to_texture`, so +/// `Features::multisampled_render_to_texture` is false there and these +/// never run. They keep the module from importing them from `gl.js`. +#[cfg(target_arch = "wasm32")] +#[allow(non_snake_case)] +mod webgl_stand_ins { + use super::{GLenum, GLint, GLsizei, GLuint}; + + pub unsafe fn glRenderbufferStorageMultisampleEXT( + _: GLenum, + _: GLsizei, + _: GLenum, + _: GLsizei, + _: GLsizei, + ) { + unreachable!() + } + + pub unsafe fn glFramebufferTexture2DMultisampleEXT( + _: GLenum, + _: GLenum, + _: GLenum, + _: GLuint, + _: GLint, + _: GLsizei, + ) { + unreachable!() + } + + pub unsafe fn glInvalidateFramebuffer(_: GLenum, _: GLsizei, _: *const GLenum) { + unreachable!() + } +} + /// Raw OpenGL bindings /// Highly unsafe, some of the functions could be missing due to incompatible GL version /// or all of them might be missing alltogether if rendering context is not a GL one. @@ -197,13 +237,27 @@ impl Texture { glGenRenderbuffers(1, &mut renderbuffer as *mut _); glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer as _); let internal_format = params.format.sized_internal_format(); - glRenderbufferStorageMultisample( - GL_RENDERBUFFER, - params.sample_count, - internal_format, - params.width as _, - params.height as _, - ); + let (width, height) = (params.width as _, params.height as _); + if params.transient && ctx.info.features.multisampled_render_to_texture { + // An implicit multisample buffer, which can share a + // framebuffer with a texture drawn into multisampled, + // and can live in tile memory alone. + glRenderbufferStorageMultisampleEXT( + GL_RENDERBUFFER, + params.sample_count, + internal_format, + width, + height, + ); + } else { + glRenderbufferStorageMultisample( + GL_RENDERBUFFER, + params.sample_count, + internal_format, + width, + height, + ); + } } return Texture { raw: TextureOrRenderbuffer::Renderbuffer(renderbuffer), @@ -493,6 +547,9 @@ pub(crate) struct RenderPassInternal { gl_fb: GLuint, color_textures: Vec, resolves: Option>, + /// The texture the pass's color resolves into on the tile, drawn into + /// multisampled in its place, with nothing to copy as the pass ends. + resolved_on_tile: Option, depth_texture: Option, } @@ -557,6 +614,7 @@ impl GlContext { vertex_buffer: 0, cur_pipeline: None, cur_pass: None, + last_pass: None, color_blend: None, alpha_blend: None, stencil: None, @@ -577,6 +635,46 @@ impl GlContext { pub fn features(&self) -> &Features { &self.info.features } + + /// Whether a pass can draw `color` into the texture it resolves into, + /// its samples kept on the tile: it's transient and multisampled, and + /// so is any depth beside it, which can then share its framebuffer. + fn resolves_on_tile(&self, color: TextureId, depth: Option) -> bool { + let transient = |texture| { + let params = self.textures.get(texture).params; + params.transient && params.sample_count > 1 + }; + self.info.features.multisampled_render_to_texture + && transient(color) + && depth.map_or(true, transient) + } + + /// Spare a pass that drawing has just come to, without clearing it, + /// from loading what its transient textures held. + fn invalidate_transient_attachments(&self, pass: RenderPass) { + if !self.info.features.multisampled_render_to_texture { + return; + } + let pass = &self.passes[pass.0]; + let transient = |texture: &TextureId| self.textures.get(*texture).params.transient; + let mut attachments: Vec = (GL_COLOR_ATTACHMENT0..) + .zip(&pass.color_textures) + .filter(|(_, texture)| transient(texture)) + .map(|(attachment, _)| attachment) + .collect(); + if pass.depth_texture.as_ref().is_some_and(transient) { + attachments.push(GL_DEPTH_ATTACHMENT); + } + if !attachments.is_empty() { + unsafe { + glInvalidateFramebuffer( + GL_FRAMEBUFFER, + attachments.len() as _, + attachments.as_ptr(), + ) + }; + } + } } fn load_shader_internal( @@ -830,9 +928,17 @@ fn gl_info() -> ContextInfo { || gl_version_string.starts_with("OpenGL ES 2"); let webgl1 = gl_version_string == "WebGL 1.0"; + // WebGL has no `GL_EXT_multisampled_render_to_texture`. + #[cfg(target_arch = "wasm32")] + let multisampled_render_to_texture = false; + #[cfg(not(target_arch = "wasm32"))] + let multisampled_render_to_texture = gl_version_string.contains("OpenGL ES 3") + && has_extension("GL_EXT_multisampled_render_to_texture"); + let features = Features { instancing: !gl2, resolve_attachments: !webgl1 && !gl2, + multisampled_render_to_texture, }; let mut glsl_support = GlslSupport::default(); @@ -881,6 +987,18 @@ fn gl_info() -> ContextInfo { } } +/// Whether the context lists `name` among its extensions. +#[cfg(not(target_arch = "wasm32"))] +fn has_extension(name: &str) -> bool { + let mut count = 0; + unsafe { glGetIntegerv(GL_NUM_EXTENSIONS, &mut count) }; + (0..count as GLuint).any(|index| { + let extension = unsafe { glGetStringi(GL_EXTENSIONS, index) }; + !extension.is_null() + && unsafe { std::ffi::CStr::from_ptr(extension as _) }.to_bytes() == name.as_bytes() + }) +} + impl RenderingBackend for GlContext { fn info(&self) -> ContextInfo { self.info.clone() @@ -1068,13 +1186,29 @@ impl RenderingBackend for GlContext { } let mut gl_fb = 0; + let resolved_on_tile = match (color_img, resolve_img) { + ([color], Some([resolve])) if self.resolves_on_tile(*color, depth_img) => { + Some(*resolve) + } + _ => None, + }; let mut resolves = None; unsafe { glGenFramebuffers(1, &mut gl_fb as *mut _); glBindFramebuffer(GL_FRAMEBUFFER, gl_fb); for (i, color_img) in color_img.iter().enumerate() { let texture = self.textures.get(*color_img); - if texture.params.sample_count > 1 { + if let Some(resolved) = resolved_on_tile { + let raw = self.textures.get(resolved).raw.texture().unwrap(); + glFramebufferTexture2DMultisampleEXT( + GL_FRAMEBUFFER, + GL_COLOR_ATTACHMENT0 + i as u32, + GL_TEXTURE_2D, + raw, + 0, + texture.params.sample_count, + ); + } else if texture.params.sample_count > 1 { let raw = texture.raw.renderbuffer().unwrap(); glFramebufferRenderbuffer( GL_FRAMEBUFFER, @@ -1123,7 +1257,7 @@ impl RenderingBackend for GlContext { glDrawBuffers(color_img.len() as _, attachments.as_ptr() as _); } - if let Some(resolve_img) = resolve_img { + if let Some(resolve_img) = resolve_img.filter(|_| resolved_on_tile.is_none()) { resolves = Some(vec![]); let resolves = resolves.as_mut().unwrap(); for (i, resolve_img) in resolve_img.iter().enumerate() { @@ -1151,6 +1285,7 @@ impl RenderingBackend for GlContext { gl_fb, color_textures: color_img.to_vec(), resolves, + resolved_on_tile, depth_texture: depth_img, }; @@ -1175,6 +1310,9 @@ impl RenderingBackend for GlContext { self.delete_texture(texture); } } + if let Some(texture) = render_pass.resolved_on_tile { + self.delete_texture(texture); + } if let Some(depth_texture) = render_pass.depth_texture { self.delete_texture(depth_texture); } @@ -1635,6 +1773,7 @@ impl RenderingBackend for GlContext { fn begin_pass(&mut self, pass: Option, action: PassAction) { self.cache.cur_pass = pass; + let carries_on = self.cache.last_pass.replace(pass) == Some(pass); let (framebuffer, w, h) = match pass { None => { let (screen_width, screen_height) = window::screen_size(); @@ -1668,7 +1807,11 @@ impl RenderingBackend for GlContext { glScissor(0, 0, w, h); } match action { - PassAction::Nothing => {} + PassAction::Nothing => { + if let (Some(pass), false) = (pass, carries_on) { + self.invalidate_transient_attachments(pass); + } + } PassAction::Clear { color, depth, @@ -1720,6 +1863,7 @@ impl RenderingBackend for GlContext { fn commit_frame(&mut self) { self.cache.clear_buffer_bindings(); self.cache.clear_texture_bindings(); + self.cache.last_pass = None; } fn draw(&self, base_element: i32, num_elements: i32, num_instances: i32) { diff --git a/src/graphics/gl/cache.rs b/src/graphics/gl/cache.rs index 2a3c1dcc7..76e3db9f4 100644 --- a/src/graphics/gl/cache.rs +++ b/src/graphics/gl/cache.rs @@ -37,6 +37,9 @@ pub struct GlCache { pub textures: [CachedTexture; MAX_SHADERSTAGE_IMAGES], pub cur_pipeline: Option, pub cur_pass: Option, + /// The pass begun last this frame, `Some(None)` for the default one, + /// which drawing carries on into while the next pass begun is the same. + pub last_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 e18ec9f75..a50f27571 100644 --- a/src/graphics/metal.rs +++ b/src/graphics/metal.rs @@ -31,6 +31,19 @@ const UNIFORM_BUFFER_ALIGN: u64 = 256; ))] const UNIFORM_BUFFER_ALIGN: u64 = 16; +/// What a color attachment keeps as its pass ends. A multisampled one +/// that isn't transient keeps its samples as well as resolving them +/// (`StoreAndMultisampleResolve`), so a later pass into the same target +/// can load them back. +fn color_store_action(resolves: bool, transient: bool) -> MTLStoreAction { + match (resolves, transient) { + (true, true) => MTLStoreAction::MultisampleResolve, + (true, false) => MTLStoreAction::StoreAndMultisampleResolve, + (false, true) => MTLStoreAction::DontCare, + (false, false) => MTLStoreAction::Store, + } +} + impl From for MTLVertexFormat { fn from(vf: VertexFormat) -> Self { match vf { @@ -295,6 +308,9 @@ pub struct MetalContext { pending_end_encoder: bool, view: ObjcId, device: ObjcId, + /// Whether the GPU can keep a texture in tile memory alone, for + /// transient textures. + memoryless: bool, current_frame_index: usize, uniform_buffers: [ObjcId; 3], // cached index_buffer from apply_bindings @@ -371,6 +387,17 @@ impl MetalContext { #[cfg(any(target_os = "ios", target_os = "tvos"))] let options = { MTLResourceOptions::CPUCacheModeWriteCombined }; + // Apple's own GPUs, which are all tile-based, can keep a + // texture in tile memory alone. `supportsFamily:` came with + // iOS 13 and macOS 10.15. + let memoryless = { + let responds: BOOL = msg_send![device, respondsToSelector: sel!(supportsFamily:)]; + responds == YES && { + let apple: BOOL = msg_send![device, supportsFamily: MTLGPUFamily::Apple1]; + apple == YES + } + }; + let uniform_buffers = [ msg_send![device, newBufferWithLength:MAX_UNIFORM_BUFFER_SIZE options:options], @@ -390,6 +417,7 @@ impl MetalContext { pending_end_encoder: false, view, device, + memoryless, buffers: vec![], shaders: vec![], pipelines: vec![], @@ -425,6 +453,7 @@ impl RenderingBackend for MetalContext { features: Features { instancing: true, resolve_attachments: false, + multisampled_render_to_texture: false, }, } } @@ -615,9 +644,9 @@ impl RenderingBackend for MetalContext { msg_send_![render_pass_desc, retain]; assert!(!render_pass_desc.is_null()); for (i, color_img) in color_img.iter().enumerate() { - let color_texture = self.textures.get(*color_img).texture; + let color = self.textures.get(*color_img); let color_attachment = msg_send_![msg_send_![render_pass_desc, colorAttachments], objectAtIndexedSubscript:i]; - msg_send_![color_attachment, setTexture: color_texture]; + msg_send_![color_attachment, setTexture: color.texture]; msg_send_![color_attachment, setLoadAction: MTLLoadAction::Clear]; // Multisample resolve: when the caller passes a resolve // texture for this attachment, downsample into it on @@ -629,17 +658,25 @@ impl RenderingBackend for MetalContext { color_attachment, setStoreAction: MTLStoreAction::MultisampleResolve ]; + } else if color.params.transient { + msg_send_![color_attachment, setStoreAction: MTLStoreAction::DontCare]; } else { msg_send_![color_attachment, setStoreAction: MTLStoreAction::Store]; } } if let Some(depth_img) = depth_img { - let depth_texture = self.textures.get(depth_img).texture; + let depth = self.textures.get(depth_img); + let depth_texture = depth.texture; let depth_attachment = msg_send_![render_pass_desc, depthAttachment]; msg_send_![depth_attachment, setTexture: depth_texture]; msg_send_![depth_attachment, setLoadAction: MTLLoadAction::Clear]; - msg_send_![depth_attachment, setStoreAction: MTLStoreAction::Store]; + let depth_store_action = if depth.params.transient { + MTLStoreAction::DontCare + } else { + MTLStoreAction::Store + }; + msg_send_![depth_attachment, setStoreAction: depth_store_action]; msg_send_![depth_attachment, setClearDepth:1.]; let stencil_attachment = msg_send_![render_pass_desc, stencilAttachment]; @@ -817,7 +854,12 @@ impl RenderingBackend for MetalContext { msg_send![self.view, colorPixelFormat]; msg_send_![descriptor, setPixelFormat: view_pixel_format]; } - msg_send_![descriptor, setStorageMode: MTLStorageMode::Private]; + let storage_mode = if params.transient && self.memoryless { + MTLStorageMode::Memoryless + } else { + MTLStorageMode::Private + }; + msg_send_![descriptor, setStorageMode: storage_mode]; if params.sample_count > 1 { // MSAA target — render-only, no mipmaps, no // shader-sample (the resolve texture is sampled @@ -1310,7 +1352,7 @@ impl RenderingBackend for MetalContext { self.command_buffer = Some(msg_send![self.command_queue, commandBuffer]); } - let (descriptor, width_px, height_px) = match pass { + let (descriptor, width_px, height_px, transient) = match pass { None => { // Read dimensions from the actual pass attachment // texture, not `screen_size()`. MTKView's @@ -1326,7 +1368,7 @@ impl RenderingBackend for MetalContext { let texture: ObjcId = msg_send_![attachment, texture]; let width: u64 = msg_send![texture, width]; let height: u64 = msg_send![texture, height]; - (descriptor, width as u32, height as u32) + (descriptor, width as u32, height as u32, false) } Some(pass) => { let pass = &self.passes[pass.0]; @@ -1338,11 +1380,13 @@ impl RenderingBackend for MetalContext { .copied() //.or(pass.depth_texture) .unwrap(); + let params = self.textures.get(texture).params; ( pass.render_pass_desc, - self.textures.get(texture).params.width, - self.textures.get(texture).params.height, + params.width, + params.height, + params.transient, ) } }; @@ -1353,19 +1397,11 @@ impl RenderingBackend for MetalContext { let color_attachments = msg_send_![descriptor, colorAttachments]; let color_attachment = msg_send_![color_attachments, objectAtIndexedSubscript: 0]; - // If the attachment has a resolve texture, use - // `StoreAndMultisampleResolve` (not just - // `MultisampleResolve`) so the multisample texture - // survives target-switch boundaries within a frame — - // a later `Load` on the same target would otherwise - // read discarded memory. let resolve_texture: ObjcId = msg_send![color_attachment, resolveTexture]; - let store_action = if resolve_texture.is_null() { - MTLStoreAction::Store - } else { - MTLStoreAction::StoreAndMultisampleResolve - }; - msg_send_![color_attachment, setStoreAction: store_action]; + msg_send_![ + color_attachment, + setStoreAction: color_store_action(!resolve_texture.is_null(), transient) + ]; match action { PassAction::Clear { color, .. } => { @@ -1376,7 +1412,12 @@ impl RenderingBackend for MetalContext { } } PassAction::Nothing => { - msg_send_![color_attachment, setLoadAction: MTLLoadAction::Load]; + let load_action = if transient { + MTLLoadAction::DontCare + } else { + MTLLoadAction::Load + }; + msg_send_![color_attachment, setLoadAction: load_action]; } } diff --git a/src/native/apple/frameworks.rs b/src/native/apple/frameworks.rs index b82187d7b..77a290512 100644 --- a/src/native/apple/frameworks.rs +++ b/src/native/apple/frameworks.rs @@ -827,6 +827,12 @@ pub enum MTLTextureUsage { PixelFormatView = 0x0010, } +#[repr(i64)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum MTLGPUFamily { + Apple1 = 1001, +} + #[repr(u64)] #[allow(non_camel_case_types)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] diff --git a/src/native/gl.rs b/src/native/gl.rs index 145b74034..5488c5985 100644 --- a/src/native/gl.rs +++ b/src/native/gl.rs @@ -400,6 +400,26 @@ gl_loader!( width: GLsizei, height: GLsizei ) -> (), + fn glRenderbufferStorageMultisampleEXT( + target: GLenum, + samples: GLsizei, + internalformat: GLenum, + width: GLsizei, + height: GLsizei + ) -> (), + fn glFramebufferTexture2DMultisampleEXT( + target: GLenum, + attachment: GLenum, + textarget: GLenum, + texture: GLuint, + level: GLint, + samples: GLsizei + ) -> (), + fn glInvalidateFramebuffer( + target: GLenum, + numAttachments: GLsizei, + attachments: *const GLenum + ) -> (), fn glDrawBuffers(n: GLsizei, bufs: *const GLenum) -> (), fn glVertexAttribDivisor(index: GLuint, divisor: GLuint) -> (), fn glBufferSubData(