From f8476b43e839c50e72e3cae1385d0a3267307140 Mon Sep 17 00:00:00 2001 From: Christian Meissl Date: Fri, 15 Sep 2023 21:58:21 +0200 Subject: [PATCH] wip --- Cargo.toml | 3 +- anvil/Cargo.toml | 2 +- build.rs | 1 + src/backend/renderer/gles/mod.rs | 157 +++++++++++++++++++++- src/backend/renderer/gles/profiler.rs | 186 ++++++++++++++++++++++++++ 5 files changed, 344 insertions(+), 5 deletions(-) create mode 100644 src/backend/renderer/gles/profiler.rs diff --git a/Cargo.toml b/Cargo.toml index 998ae77fdd84..b5d8b66b9b48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,8 @@ x11rb = { version = "0.11.1", optional = true } xkbcommon = { version = "0.5.0", features = ["wayland"]} scan_fmt = { version = "0.2.3", default-features = false } encoding = { version = "0.2.33", optional = true } -profiling = "1.0" +profiling = { version = "1.0", git = "https://github.com/cmeissl/profiling.git", branch = "bump_tracy_0.16.2" } +tracy-client = { version = "0.16.2" } [dev-dependencies] clap = { version = "4", features = ["derive"] } diff --git a/anvil/Cargo.toml b/anvil/Cargo.toml index 317712e178d4..2f0983b5be0f 100644 --- a/anvil/Cargo.toml +++ b/anvil/Cargo.toml @@ -19,7 +19,7 @@ xkbcommon = "0.5.0" renderdoc = {version = "0.11.0", optional = true} smithay-drm-extras = {path = "../smithay-drm-extras", optional = true} puffin_http = { version = "0.9", optional = true } -profiling = { version = "1.0" } +profiling = { version = "1.0", git = "https://github.com/cmeissl/profiling.git", branch = "bump_tracy_0.16.2" } [dependencies.smithay] default-features = false diff --git a/build.rs b/build.rs index b0a0532e99dc..516deedc4661 100644 --- a/build.rs +++ b/build.rs @@ -64,6 +64,7 @@ fn gl_generate() { "GL_EXT_texture_format_BGRA8888", "GL_EXT_unpack_subimage", "GL_OES_EGL_sync", + "GL_EXT_disjoint_timer_query", ], ) .write_bindings(gl_generator::StructGenerator, &mut file) diff --git a/src/backend/renderer/gles/mod.rs b/src/backend/renderer/gles/mod.rs index 37b2b28fcde3..9c5b9b397fde 100644 --- a/src/backend/renderer/gles/mod.rs +++ b/src/backend/renderer/gles/mod.rs @@ -24,6 +24,7 @@ use std::cell::RefCell; pub mod element; mod error; pub mod format; +mod profiler; mod shaders; mod texture; mod uniform; @@ -416,6 +417,9 @@ pub struct GlesRenderer { // debug span: tracing::Span, gl_debug_span: Option<*mut tracing::Span>, + + // profiling + profiler: profiler::GpuProfiler, } /// Handle to the currently rendered frame during [`GlesRenderer::render`](Renderer::render). @@ -432,6 +436,8 @@ pub struct GlesFrame<'frame> { tex_program_override: Option<(GlesTexProgram, Vec>)>, finished: AtomicBool, span: EnteredSpan, + + gpu_span: Option, } impl<'frame> fmt::Debug for GlesFrame<'frame> { @@ -732,6 +738,9 @@ impl GlesRenderer { .insert_if_missing(|| RendererId(next_renderer_id())); drop(_guard); + + let profiler = profiler::GpuProfiler::new(&gl); + let renderer = GlesRenderer { gl, egl: context, @@ -760,6 +769,8 @@ impl GlesRenderer { _not_send: std::ptr::null_mut(), span, gl_debug_span, + + profiler, }; renderer.egl.unbind()?; Ok(renderer) @@ -786,6 +797,9 @@ impl GlesRenderer { let mut i = 0; while i != self.buffers.len() { if self.buffers[i].dmabuf.is_gone() { + let _scope = self + .profiler + .scope(tracy_client::span_location!("cleanup dmabuf"), &self.gl); let old = self.buffers.remove(i); unsafe { self.gl.DeleteFramebuffers(1, &old.fbo as *const _); @@ -797,6 +811,9 @@ impl GlesRenderer { } } for resource in self.destruction_callback.try_iter() { + let _scope = self + .profiler + .scope(tracy_client::span_location!("cleanup resources"), &self.gl); match resource { CleanupResource::Texture(texture) => unsafe { self.gl.DeleteTextures(1, &texture); @@ -933,6 +950,9 @@ impl ImportMemWl for GlesRenderer { }), ); + let _scope = self + .profiler + .scope(tracy_client::span_location!("import_shm_buffer"), &self.gl); unsafe { self.gl.BindTexture(ffi::TEXTURE_2D, texture.0.texture); self.gl @@ -1047,6 +1067,9 @@ impl ImportMem for GlesRenderer { } let texture = GlesTexture(Rc::new({ + let _scope = self + .profiler + .scope(tracy_client::span_location!("import_memory"), &self.gl); let mut tex = 0; unsafe { self.gl.GenTextures(1, &mut tex); @@ -1110,6 +1133,9 @@ impl ImportMem for GlesRenderer { return Err(GlesError::UnexpectedSize); } + let _scope = self + .profiler + .scope(tracy_client::span_location!("update_memory"), &self.gl); unsafe { self.gl.BindTexture(ffi::TEXTURE_2D, texture.0.texture); self.gl @@ -1282,7 +1308,7 @@ impl ImportDmaWl for GlesRenderer {} impl GlesRenderer { #[profiling::function] - fn existing_dmabuf_texture(&self, buffer: &Dmabuf) -> Result, GlesError> { + fn existing_dmabuf_texture(&mut self, buffer: &Dmabuf) -> Result, GlesError> { let existing_texture = self .dmabuf_cache .iter() @@ -1306,11 +1332,14 @@ impl GlesRenderer { #[profiling::function] fn import_egl_image( - &self, + &mut self, image: EGLImage, is_external: bool, tex: Option, ) -> Result { + let _scope = self + .profiler + .scope(tracy_client::span_location!("import_egl_image"), &self.gl); let tex = tex.unwrap_or_else(|| unsafe { let mut tex = 0; self.gl.GenTextures(1, &mut tex); @@ -1357,6 +1386,9 @@ impl ExportMem for GlesRenderer { .ok_or(GlesError::UnknownPixelFormat)?; let (_, format, layout) = fourcc_to_gl_formats(fourcc).ok_or(GlesError::UnknownPixelFormat)?; + let _scope = self + .profiler + .scope(tracy_client::span_location!("copy_framebuffer"), &self.gl); let mut pbo = 0; let err = unsafe { self.gl.GetError(); // clear errors @@ -1415,6 +1447,9 @@ impl ExportMem for GlesRenderer { let (_, format, layout) = fourcc_to_gl_formats(fourcc).ok_or(GlesError::UnknownPixelFormat)?; let bpp = gl_bpp(format, layout).expect("We check the format before") / 8; + let gpu_span = self + .profiler + .scope(tracy_client::span_location!("copy_texture"), &self.gl); let err = unsafe { self.gl.GetError(); // clear errors self.gl.GenBuffers(1, &mut pbo); @@ -1439,6 +1474,7 @@ impl ExportMem for GlesRenderer { self.gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, 0); self.gl.GetError() }; + std::mem::drop(gpu_span); // restore old framebuffer self.unbind()?; @@ -1473,6 +1509,9 @@ impl ExportMem for GlesRenderer { let mapping_ptr = texture_mapping.mapping.load(Ordering::SeqCst); let ptr = if mapping_ptr.is_null() { unsafe { + let _scope = self + .profiler + .scope(tracy_client::span_location!("map_texture"), &self.gl); self.gl.BindBuffer(ffi::PIXEL_PACK_BUFFER, texture_mapping.pbo); let ptr = self .gl @@ -1496,12 +1535,18 @@ impl ExportMem for GlesRenderer { impl GlesRenderer { #[profiling::function] - fn create_shadow_buffer(&self, size: Size) -> Result, GlesError> { + fn create_shadow_buffer( + &mut self, + size: Size, + ) -> Result, GlesError> { trace!(?size, "Creating shadow framebuffer"); self.capabilities .contains(&Capability::ColorTransformations) .then(|| { + let _scope = self + .profiler + .scope(tracy_client::span_location!("create_shadow_buffer"), &self.gl); let mut tex = unsafe { let mut tex = 0; self.gl.GenTextures(1, &mut tex); @@ -1628,6 +1673,9 @@ impl Bind for GlesRenderer { .create_image_from_dmabuf(&dmabuf) .map_err(GlesError::BindBufferEGLError)?; + let scope = self + .profiler + .enter(tracy_client::span_location!("bind dmabuf"), &self.gl); unsafe { let mut rbo = 0; self.gl.GenRenderbuffers(1, &mut rbo as *mut _); @@ -1653,6 +1701,7 @@ impl Bind for GlesRenderer { return Err(GlesError::FramebufferBindingError); } let shadow = self.create_shadow_buffer(dmabuf.0.size)?; + self.profiler.exit(&self.gl, scope); let buf = GlesBuffer { dmabuf: dmabuf.weak(), @@ -1685,6 +1734,9 @@ impl Bind for GlesRenderer { self.unbind()?; self.make_current()?; + let scope = self + .profiler + .enter(tracy_client::span_location!("bind texture"), &self.gl); let mut fbo = 0; unsafe { self.gl.GenFramebuffers(1, &mut fbo as *mut _); @@ -1706,6 +1758,7 @@ impl Bind for GlesRenderer { } let shadow = self.create_shadow_buffer(texture.size())?; + self.profiler.exit(&self.gl, scope); self.target = Some(GlesTarget::Texture { texture, shadow, @@ -1741,6 +1794,9 @@ impl Offscreen for GlesRenderer { return Err(GlesError::UnsupportedPixelLayout); } + let scope = self + .profiler + .scope(tracy_client::span_location!("create texture buffer"), &self.gl); let tex = unsafe { let mut tex = 0; self.gl.GenTextures(1, &mut tex); @@ -1758,6 +1814,7 @@ impl Offscreen for GlesRenderer { ); tex }; + std::mem::drop(scope); Ok(unsafe { GlesTexture::from_raw(self, Some(internal), !has_alpha, tex, size) }) } @@ -1770,6 +1827,9 @@ impl Bind for GlesRenderer { self.unbind()?; self.make_current()?; + let scope = self + .profiler + .enter(tracy_client::span_location!("bind renderbuffer"), &self.gl); let mut fbo = 0; unsafe { self.gl.GenFramebuffers(1, &mut fbo as *mut _); @@ -1792,6 +1852,7 @@ impl Bind for GlesRenderer { } let shadow = self.create_shadow_buffer(renderbuffer.size())?; + self.profiler.exit(&self.gl, scope); self.target = Some(GlesTarget::Renderbuffer { buf: renderbuffer, shadow, @@ -1828,6 +1889,10 @@ impl Offscreen for GlesRenderer { return Err(GlesError::UnsupportedPixelLayout); } + let _scope = self.profiler.scope( + tracy_client::span_location!("create renderbuffer buffer"), + &self.gl, + ); unsafe { let mut rbo = 0; self.gl.GenRenderbuffers(1, &mut rbo); @@ -1927,6 +1992,9 @@ impl GlesRenderer { }, } + let scope = self + .profiler + .scope(tracy_client::span_location!("blit"), &self.gl); match src_target { GlesTarget::Image { ref buf, .. } => unsafe { self.gl.BindFramebuffer(ffi::READ_FRAMEBUFFER, buf.fbo) @@ -1954,6 +2022,7 @@ impl GlesRenderer { let status = unsafe { self.gl.CheckFramebufferStatus(ffi::FRAMEBUFFER) }; if status != ffi::FRAMEBUFFER_COMPLETE { + std::mem::drop(scope); let _ = self.unbind(); return Err(GlesError::FramebufferBindingError); } @@ -1992,8 +2061,12 @@ impl Unbind for GlesRenderer { unsafe { self.egl.make_current()?; } + let scope = self + .profiler + .scope(tracy_client::span_location!("unbind"), &self.gl); unsafe { self.gl.BindFramebuffer(ffi::FRAMEBUFFER, 0) }; self.target = None; + std::mem::drop(scope); self.egl.unbind()?; Ok(()) } @@ -2087,6 +2160,10 @@ impl GlesRenderer { ) -> Result { self.make_current()?; + let _scope = self.profiler.scope( + tracy_client::span_location!("compile_custom_pixel_shader"), + &self.gl, + ); let shader = format!("#version 100\n{}", src.as_ref()); let program = unsafe { link_program(&self.gl, shaders::VERTEX_SHADER, &shader)? }; let debug_shader = format!("#version 100\n#define {}\n{}", shaders::DEBUG_FLAGS, src.as_ref()); @@ -2268,6 +2345,13 @@ impl Renderer for GlesRenderer { transform: Transform, ) -> Result, Self::Error> { self.make_current()?; + self.profiler.collect(&self.gl); + let gpu_span = self + .profiler + .enter(tracy_client::span_location!("render"), &self.gl); + let scope = self + .profiler + .enter(tracy_client::span_location!("setup"), &self.gl); unsafe { self.gl.Viewport(0, 0, output_size.w, output_size.h); @@ -2284,6 +2368,9 @@ impl Renderer for GlesRenderer { .map(|target| target.has_shadow()) .unwrap_or(false) { + let _scope = self + .profiler + .scope(tracy_client::span_location!("setup shadow buffer"), &self.gl); // Enable stencil testing and clear the shadow buffer for blending onto the actual framebuffer in finish self.gl.Enable(ffi::STENCIL_TEST); self.gl.StencilFunc(ffi::ALWAYS, 1, ffi::types::GLuint::MAX); @@ -2292,6 +2379,7 @@ impl Renderer for GlesRenderer { self.gl.Clear(ffi::COLOR_BUFFER_BIT | ffi::STENCIL_BUFFER_BIT); } } + self.profiler.exit(&self.gl, scope); // Handle the width/height swap when the output is rotated by 90°/270°. if let Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270 = transform { @@ -2330,6 +2418,8 @@ impl Renderer for GlesRenderer { tex_program_override: None, finished: AtomicBool::new(false), span, + + gpu_span: Some(gpu_span), }) } @@ -2446,6 +2536,10 @@ impl<'frame> Frame for GlesFrame<'frame> { return Ok(()); } + let scope = self + .renderer + .profiler + .enter(tracy_client::span_location!("clear"), &self.renderer.gl); unsafe { self.renderer.gl.Disable(ffi::BLEND); } @@ -2456,6 +2550,8 @@ impl<'frame> Frame for GlesFrame<'frame> { self.renderer.gl.Enable(ffi::BLEND); self.renderer.gl.BlendFunc(ffi::ONE, ffi::ONE_MINUS_SRC_ALPHA); } + self.renderer.profiler.exit(&self.renderer.gl, scope); + res } @@ -2473,6 +2569,10 @@ impl<'frame> Frame for GlesFrame<'frame> { let is_opaque = color[3] == 1f32; + let scope = self + .renderer + .profiler + .enter(tracy_client::span_location!("draw_solid"), &self.renderer.gl); if is_opaque { unsafe { self.renderer.gl.Disable(ffi::BLEND); @@ -2487,6 +2587,7 @@ impl<'frame> Frame for GlesFrame<'frame> { self.renderer.gl.BlendFunc(ffi::ONE, ffi::ONE_MINUS_SRC_ALPHA); } } + self.renderer.profiler.exit(&self.renderer.gl, scope); res } @@ -2529,6 +2630,10 @@ impl<'frame> GlesFrame<'frame> { return Ok(SyncPoint::signaled()); } + let finish_gpu_span = self + .renderer + .profiler + .enter(tracy_client::span_location!("finish_internal"), &self.renderer.gl); unsafe { self.renderer.gl.Disable(ffi::SCISSOR_TEST); self.renderer.gl.Disable(ffi::BLEND); @@ -2541,6 +2646,10 @@ impl<'frame> GlesFrame<'frame> { &self.renderer.egl, Some((shadow.fbo, shadow.stencil)), )?; + let _scope = self.renderer.profiler.scope( + tracy_client::span_location!("draw shadow buffer"), + &self.renderer.gl, + ); unsafe { self.renderer .gl @@ -2592,6 +2701,15 @@ impl<'frame> GlesFrame<'frame> { } } + self.renderer.profiler.exit(&self.renderer.gl, finish_gpu_span); + if let Some(span) = self.gpu_span.take() { + self.renderer.profiler.exit(&self.renderer.gl, span); + } + + unsafe { + self.renderer.gl.Flush(); + } + // if we support egl fences we should use it if self.renderer.capabilities.contains(&Capability::Fencing) { if let Ok(fence) = EGLFence::create(self.renderer.egl.display()) { @@ -2667,6 +2785,10 @@ impl<'frame> GlesFrame<'frame> { .collect::>(); let gl = &self.renderer.gl; + let _scope = self + .renderer + .profiler + .scope(tracy_client::span_location!("draw_solid"), &self.renderer.gl); unsafe { gl.UseProgram(self.renderer.solid_program.program); gl.Uniform4f( @@ -2921,7 +3043,15 @@ impl<'frame> GlesFrame<'frame> { // render let gl = &self.renderer.gl; + let scope = self + .renderer + .profiler + .enter(tracy_client::span_location!("render_texture"), gl); unsafe { + let scope = self + .renderer + .profiler + .enter(tracy_client::span_location!("setup"), gl); gl.ActiveTexture(ffi::TEXTURE0); gl.BindTexture(target, tex.0.texture); gl.TexParameteri( @@ -3007,14 +3137,24 @@ impl<'frame> GlesFrame<'frame> { 0, std::ptr::null(), ); + self.renderer.profiler.exit(gl, scope); let damage_len = (damage.len() / 4) as i32; if self.renderer.capabilities.contains(&Capability::Instancing) { + let scope = self + .renderer + .profiler + .enter(tracy_client::span_location!("draw instanced"), gl); gl.VertexAttribDivisor(program.attrib_vert as u32, 0); gl.VertexAttribDivisor(program.attrib_vert_position as u32, 1); gl.DrawArraysInstanced(ffi::TRIANGLE_STRIP, 0, 4, damage_len); + self.renderer.profiler.exit(gl, scope); } else { + let scope = self + .renderer + .profiler + .enter(tracy_client::span_location!("draw batched"), gl); // When we have more than 10 rectangles, draw them in batches of 10. for i in 0..(damage_len - 1) / 10 { gl.DrawArrays(ffi::TRIANGLES, 0, 6); @@ -3034,13 +3174,20 @@ impl<'frame> GlesFrame<'frame> { // Draw the up to 10 remaining rectangles. let count = ((damage_len - 1) % 10 + 1) * 6; gl.DrawArrays(ffi::TRIANGLES, 0, count); + self.renderer.profiler.exit(gl, scope); } + let scope = self + .renderer + .profiler + .enter(tracy_client::span_location!("cleanup"), gl); gl.BindBuffer(ffi::ARRAY_BUFFER, 0); gl.BindTexture(target, 0); gl.DisableVertexAttribArray(program.attrib_vert as u32); gl.DisableVertexAttribArray(program.attrib_vert_position as u32); + self.renderer.profiler.exit(gl, scope); } + self.renderer.profiler.exit(gl, scope); Ok(()) } @@ -3107,6 +3254,10 @@ impl<'frame> GlesFrame<'frame> { // render let gl = &self.renderer.gl; + let _scope = self + .renderer + .profiler + .scope(tracy_client::span_location!("render_pixel_shader_to"), gl); unsafe { gl.UseProgram(program.program); diff --git a/src/backend/renderer/gles/profiler.rs b/src/backend/renderer/gles/profiler.rs new file mode 100644 index 000000000000..48c97c4e889d --- /dev/null +++ b/src/backend/renderer/gles/profiler.rs @@ -0,0 +1,186 @@ +use super::ffi; + +const MAX_QUERIES: usize = 64 * 1024; + +pub struct GpuProfiler { + context: tracy_client::GpuContext, + pool: Vec, + index: usize, + tracepoints: Vec, +} + +impl GpuProfiler { + pub fn new(gl: &ffi::Gles2) -> Self { + let mut queries = [0; MAX_QUERIES]; + unsafe { + gl.GenQueriesEXT(queries.len() as ffi::types::GLsizei, queries.as_mut_ptr()); + } + let pool = queries + .chunks_exact(2) + .map(|pairs| { + let (entered, exited) = unsafe { (*pairs.get_unchecked(0), *pairs.get_unchecked(1)) }; + GpuTracepoint { entered, exited } + }) + .collect::>(); + + let client = tracy_client::Client::start(); + + let mut bits = 0; + unsafe { gl.GetQueryivEXT(ffi::TIMESTAMP_EXT, ffi::QUERY_COUNTER_BITS_EXT, &mut bits) }; + + let mut gpu_timestamp = 0; + unsafe { gl.GetInteger64v(ffi::TIMESTAMP_EXT, &mut gpu_timestamp) }; + + let context = client + .new_gpu_context( + Some("GlesRenderer"), + tracy_client::GpuContextType::OpenGL, + gpu_timestamp, + 1.0, + ) + .unwrap(); + + Self { + context, + pool, + index: 0, + tracepoints: Vec::with_capacity(MAX_QUERIES / 2), + } + } + + pub fn scope<'a, 'b>( + &'a mut self, + span_location: &'static tracy_client::SpanLocation, + gl: &'b ffi::Gles2, + ) -> ScopedGpuTracepoint<'a, 'b> { + let tracepoint = unsafe { *self.pool.get_unchecked(self.index) }; + self.index = (self.index + 1) % self.pool.len(); + + unsafe { + gl.QueryCounterEXT(tracepoint.entered, ffi::TIMESTAMP_EXT); + } + + let span = self.context.span(span_location).unwrap(); + + ScopedGpuTracepoint { + span: Some(EnteredGpuTracepoint { + span: Some(GpuSpan { tracepoint, span }), + }), + gl, + profiler: self, + } + } + + pub fn enter( + &mut self, + span_location: &'static tracy_client::SpanLocation, + gl: &ffi::Gles2, + ) -> EnteredGpuTracepoint { + let tracepoint = unsafe { *self.pool.get_unchecked(self.index) }; + self.index = (self.index + 1) % self.pool.len(); + + unsafe { + gl.QueryCounterEXT(tracepoint.entered, ffi::TIMESTAMP_EXT); + } + + let span = self.context.span(span_location).unwrap(); + + EnteredGpuTracepoint { + span: Some(GpuSpan { tracepoint, span }), + } + } + + pub fn exit(&mut self, gl: &ffi::Gles2, mut exited: EnteredGpuTracepoint) { + let Some(mut span) = exited.span.take() else { + return; + }; + + unsafe { + gl.QueryCounterEXT(span.tracepoint.exited, ffi::TIMESTAMP_EXT); + } + span.span.end_zone(); + + self.tracepoints.push(ExitedGpuTracepoint { span }); + } + + pub fn collect(&mut self, gl: &ffi::Gles2) { + let mut i = 0; + while i != self.tracepoints.len() { + let mut available: ffi::types::GLuint = 0; + unsafe { + gl.GetQueryObjectuivEXT( + self.tracepoints[i].span.tracepoint.exited, + ffi::QUERY_RESULT_AVAILABLE, + &mut available, + ); + if gl.GetError() != ffi::NO_ERROR { + self.tracepoints.remove(i); + continue; + } + } + + if available == 1 { + let tracepoint = self.tracepoints.remove(i); + let mut start_timestamp = 0; + let mut end_timestamp = 0; + unsafe { + gl.GetQueryObjecti64vEXT( + tracepoint.span.tracepoint.entered, + ffi::QUERY_RESULT, + &mut start_timestamp, + ); + gl.GetQueryObjecti64vEXT( + tracepoint.span.tracepoint.exited, + ffi::QUERY_RESULT, + &mut end_timestamp, + ); + } + tracepoint + .span + .span + .upload_timestamp(start_timestamp, end_timestamp); + } else { + i += 1; + } + } + } +} + +#[derive(Debug, Clone, Copy)] +struct GpuTracepoint { + entered: ffi::types::GLuint, + exited: ffi::types::GLuint, +} + +struct GpuSpan { + tracepoint: GpuTracepoint, + span: tracy_client::GpuSpan, +} + +pub struct EnteredGpuTracepoint { + span: Option, +} + +pub struct ScopedGpuTracepoint<'a, 'b> { + span: Option, + profiler: &'a mut GpuProfiler, + gl: &'b ffi::Gles2, +} + +impl<'a, 'b> Drop for ScopedGpuTracepoint<'a, 'b> { + fn drop(&mut self) { + if let Some(span) = self.span.take() { + self.profiler.exit(self.gl, span); + } + } +} + +struct ExitedGpuTracepoint { + span: GpuSpan, +} + +impl Drop for EnteredGpuTracepoint { + fn drop(&mut self) { + debug_assert!(self.span.is_none()); + } +}