From d26c1172b10cb0cfa100a20c51a6f5a5f50777aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benoi=CC=82t=20Rouleau?= Date: Fri, 9 Oct 2026 21:31:49 -0400 Subject: [PATCH 1/2] metal: keep transient render textures' samples in tile memory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A multisampled render target's samples are stored at the end of every pass (`StoreAndMultisampleResolve`), so a later pass into the same target can load them back. An app that clears the target each frame and only reads what it resolves into never loads them, but still writes them out every frame, and the multisampled texture still takes up memory: about 50 MB at an iPhone 17 Pro's full resolution with 4× antialiasing. `TextureParams::transient` says a render texture's contents only last as long as the pass drawing into it. Metal then stores nothing of it when a pass ends but what it resolves into (`MultisampleResolve`, or `DontCare` without a resolve texture), a pass that doesn't clear it starts with `DontCare`, and on a GPU with tile memory, every Apple GPU, the texture is `MTLStorageModeMemoryless` and takes up no memory at all. Consecutive passes into the same target are still merged into one encoder, so a target drawn by one draw call after another keeps its samples throughout. A transient depth texture isn't stored either. OpenGL keeps a transient texture as it does any other, which the contract allows. Everything else is unchanged for textures that aren't transient. Drawing a full-screen 4× target that's cleared each frame and resolved once took an iPhone 17 Pro's GPU from 5.08 to 4.85 ms a frame, and the app's footprint from 319 to 269 MB. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent --- src/graphics.rs | 11 +++++ src/graphics/metal.rs | 84 +++++++++++++++++++++++++--------- src/native/apple/frameworks.rs | 6 +++ 3 files changed, 79 insertions(+), 22 deletions(-) diff --git a/src/graphics.rs b/src/graphics.rs index dbdfffc65..7ca537078 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -400,6 +400,15 @@ 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. OpenGL keeps them + /// as it does any texture's. + pub transient: bool, } impl Default for TextureParams { @@ -415,6 +424,7 @@ impl Default for TextureParams { height: 0, allocate_mipmaps: false, sample_count: 1, + transient: false, } } } @@ -1160,6 +1170,7 @@ pub trait RenderingBackend { mipmap_filter: MipmapFilterMode::None, allocate_mipmaps: false, sample_count: 1, + transient: false, }, ) } diff --git a/src/graphics/metal.rs b/src/graphics/metal.rs index e18ec9f75..973426be5 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![], @@ -615,9 +643,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 +657,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 +853,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 +1351,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 +1367,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 +1379,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 +1396,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 +1411,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)] From 66c19b20fc502590621408d5fe2706a08fc3f323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benoi=CC=82t=20Rouleau?= Date: Sat, 10 Oct 2026 00:48:57 -0400 Subject: [PATCH 2/2] gl: keep transient multisampled samples on the tile with EXT_multisampled_render_to_texture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On OpenGL, a multisampled render pass draws into a renderbuffer, and `end_render_pass` blits it into the resolve texture. On a tile-based GPU, the blit makes the GPU write every sample out to memory and read them all back: about 40 MB each way per resolve of a full-screen 4× target on a 1080×2400 phone. OpenGL ES's `GL_EXT_multisampled_render_to_texture` lets a framebuffer draw into a single-sampled texture multisampled, keeping the samples in tile memory and resolving them as the GPU writes the tile out. `TextureParams::transient` already says a render texture's contents only last as long as the pass drawing into it, which is that extension's contract. On OpenGL ES 3 with the extension, which `Features::multisampled_render_to_texture` reports: - A pass with one transient multisampled color attachment and a resolve texture, and no depth or a transient one, attaches the resolve texture with `glFramebufferTexture2DMultisampleEXT` instead of the renderbuffer, and `end_render_pass` has nothing to blit. The resolve texture holds what's drawn once drawing leaves the pass. - A transient multisampled texture's storage is an implicit multisample buffer (`glRenderbufferStorageMultisampleEXT`), which can share such a framebuffer and live in tile memory alone. - A pass that doesn't clear starts by invalidating its transient attachments (`glInvalidateFramebuffer`), unless it carries on drawing into the target the last pass begun drew into, so the GPU doesn't load them. Everything else is unchanged, and so is any other context. WebGL never reports the feature, and stand-ins keep the module from importing the new functions from `gl.js`. On a Pixel 6 (Mali-G78), at 60 Hz, a macroquad game that draws each frame into a full-screen transient 4× target, leaves it mid-frame to blur it and copies it to the screen held 60 fps at a GPU clock of 302 MHz instead of 471, and its graphics memory went from 184 to 146 MB. That was measured with buffer updates no longer waiting on the driver: before that, those waits held the frame rate down, and they grew as the lighter frames let the GPU clock down. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent --- src/graphics.rs | 15 +++- src/graphics/gl.rs | 164 ++++++++++++++++++++++++++++++++++++--- src/graphics/gl/cache.rs | 3 + src/graphics/metal.rs | 1 + src/native/gl.rs | 20 +++++ 5 files changed, 191 insertions(+), 12 deletions(-) diff --git a/src/graphics.rs b/src/graphics.rs index 7ca537078..8efaec6b7 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -406,8 +406,11 @@ pub struct TextureParams { /// 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. OpenGL keeps them - /// as it does any texture's. + /// 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, } @@ -592,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 { @@ -599,6 +609,7 @@ impl Default for Features { Features { instancing: true, resolve_attachments: true, + multisampled_render_to_texture: 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 973426be5..a50f27571 100644 --- a/src/graphics/metal.rs +++ b/src/graphics/metal.rs @@ -453,6 +453,7 @@ impl RenderingBackend for MetalContext { features: Features { instancing: true, resolve_attachments: false, + multisampled_render_to_texture: false, }, } } 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(