From 16d3e972cba0b86654c73b5761e31e3692fb642e Mon Sep 17 00:00:00 2001 From: Guy Sviry Date: Fri, 26 Jan 2024 18:33:27 +0200 Subject: [PATCH 1/4] Lookup-texture style animations * add `lt/on` `lt/set` commands to set view as each other's lookup textures * add lookuptex.{vert,frag} shaders to render the lookup * generate initial lookup table values in `lt/on` views * had to refactor Renderer.view_data into RefCell values * so other views could be referenced during the redering. Note this is a pretty alpha stage, just got the shader to work really. More work is needed to pack this in a sane user experience. --- src/cmd.rs | 9 + src/draw.rs | 14 ++ src/gl/data/lookuptex.frag | 37 +++++ src/gl/data/lookuptex.vert | 28 ++++ src/gl/mod.rs | 328 ++++++++++++++++++++++++++----------- src/session.rs | 28 ++++ src/view.rs | 60 +++++++ 7 files changed, 412 insertions(+), 92 deletions(-) create mode 100644 src/gl/data/lookuptex.frag create mode 100644 src/gl/data/lookuptex.vert diff --git a/src/cmd.rs b/src/cmd.rs index 2498052a..0b5bd850 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -122,6 +122,9 @@ pub enum Command { ViewNext, ViewPrev, + LookupTextureOn, + LookupTextureSet(i32), + Noop, } @@ -1051,6 +1054,12 @@ impl Default for Commands { .then(tuple::(integer().label(""), integer().label(""))) .map(|((_, i), (x, y))| Command::PaintPalette(i, x, y)) }) + .command("lt/on", "Set current view as lookup texture", |p| { + p.value(Command::LookupTextureOn) }) + .command("lt/set", "Set another view as lookup texture to current view", |p| { + p.then(integer::().label("")) + .map(|(_, d)| Command::LookupTextureSet(d)) + }) } } diff --git a/src/draw.rs b/src/draw.rs index 4dcb4ed4..abe263ca 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -730,6 +730,20 @@ pub fn draw_view_animation(session: &Session, v: &View) -> sprite2d::Batch ) } +pub fn draw_view_lookuptexture_animation(session: &Session, v: &View) -> sprite2d::Batch { + sprite2d::Batch::singleton( + v.width(), + v.fh, + *v.animation.val(), + Rect::new(-(v.fw as f32) * 2., 0., -(v.fw as f32), v.fh as f32) * v.zoom + (session.offset + v.offset), + self::VIEW_LAYER, + Rgba::TRANSPARENT, + 1., + Repeat::default(), + ) +} + + pub fn draw_view_composites(session: &Session, v: &View) -> sprite2d::Batch { let mut batch = sprite2d::Batch::new(v.width(), v.fh); diff --git a/src/gl/data/lookuptex.frag b/src/gl/data/lookuptex.frag new file mode 100644 index 00000000..df1aa790 --- /dev/null +++ b/src/gl/data/lookuptex.frag @@ -0,0 +1,37 @@ +uniform sampler2D tex; +uniform sampler2D ltex; +uniform vec2 ltexreg; // lookup texture region normalization vector + +in vec2 f_uv; +in vec4 f_color; +in float f_opacity; + +out vec4 fragColor; + +vec3 linearTosRGB(vec3 linear) { + vec3 lower = linear * 12.92; + vec3 higher = 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055; + + // Use smoothstep for a smoother transition + vec3 transition = smoothstep(vec3(0.0031308 - 0.00001), vec3(0.0031308 + 0.00001), linear); + + return mix(lower, higher, transition); +} + +void main() { + vec4 texel = texture(tex, f_uv); + texel = vec4(linearTosRGB(texel.rgb), texel.a); // Convert to linear space + texel.rg = texel.rg * ltexreg; + if (texel.a > 0.0) { // Non-transparent pixel + vec4 lt_texel = texture(ltex, texel.rg); + fragColor = vec4( + mix(lt_texel.rgb, f_color.rgb, f_color.a), + lt_texel.a * f_opacity + ); + } else { + fragColor = vec4( + mix(texel.rgb, f_color.rgb, f_color.a), + texel.a * f_opacity + ); + } +} diff --git a/src/gl/data/lookuptex.vert b/src/gl/data/lookuptex.vert new file mode 100644 index 00000000..7bcc2f1a --- /dev/null +++ b/src/gl/data/lookuptex.vert @@ -0,0 +1,28 @@ +uniform mat4 ortho; +uniform mat4 transform; + +in vec3 position; +in vec2 uv; +in vec4 color; +in float opacity; + +out vec2 f_uv; +out vec4 f_color; +out float f_opacity; + +// Convert an sRGB color to linear space. +vec3 linearize(vec3 srgb) { + bvec3 cutoff = lessThan(srgb, vec3(0.04045)); + vec3 higher = pow((srgb + vec3(0.055)) / vec3(1.055), vec3(2.4)); + vec3 lower = srgb / vec3(12.92); + + return mix(higher, lower, cutoff); +} + +void main() { + f_color = vec4(linearize(color.rgb), color.a); + f_uv = uv; + f_opacity = opacity; + + gl_Position = ortho * transform * vec4(position, 1.0); +} diff --git a/src/gl/mod.rs b/src/gl/mod.rs index c1cc93c4..26d949b1 100644 --- a/src/gl/mod.rs +++ b/src/gl/mod.rs @@ -31,6 +31,7 @@ use luminance::{ use luminance_derive::{Semantics, UniformInterface, Vertex}; use luminance_gl::gl33; +use std::cell::RefCell; use std::collections::BTreeMap; use std::error::Error; use std::fmt; @@ -39,6 +40,7 @@ use std::mem; use std::time; type Backend = gl33::GL33; +type V2 = [f32; 2]; type M44 = [[f32; 4]; 4]; const SAMPLER: Sampler = Sampler { @@ -127,6 +129,24 @@ struct Screen2dInterface { framebuffer: Uniform>, } +#[derive(UniformInterface)] +struct Lookuptex2dInterface { + tex: Uniform>, + ltex: Uniform>, + ltexreg: Uniform, + ortho: Uniform, + transform: Uniform, +} + +#[repr(C)] +#[derive(Copy, Clone, Vertex)] +#[vertex(sem = "VertexSemantics")] +#[rustfmt::skip] +struct Lookuptex2dVertex { + #[allow(dead_code)] position: VertexPosition, + #[allow(dead_code)] uv: VertexUv, +} + pub struct Renderer { pub win_size: LogicalSize, @@ -153,8 +173,9 @@ pub struct Renderer { shape2d: Program, cursor2d: Program, screen2d: Program, + lookuptex2d: Program, - view_data: BTreeMap, + view_data: BTreeMap>, } struct LayerData { @@ -239,6 +260,7 @@ struct ViewData { layer: LayerData, staging_fb: Framebuffer, anim_tess: Option>, + anim_lt_tess: Option>, layer_tess: Option>, } @@ -256,6 +278,7 @@ impl ViewData { layer: LayerData::new(w, h, pixels, ctx), staging_fb, anim_tess: None, + anim_lt_tess: None, layer_tess: None, } } @@ -399,6 +422,10 @@ impl<'a> renderer::Renderer<'a> for Renderer { include_str!("data/screen.vert"), include_str!("data/screen.frag"), ); + let lookuptex2d = ctx.program::( + include_str!("data/lookuptex.vert"), + include_str!("data/lookuptex.frag"), + ); let physical = win_size.to_physical(scale_factor); let present_fb = @@ -450,6 +477,7 @@ impl<'a> renderer::Renderer<'a> for Renderer { shape2d, cursor2d, screen2d, + lookuptex2d, font, cursors, checker, @@ -495,6 +523,7 @@ impl<'a> renderer::Renderer<'a> for Renderer { shape2d, cursor2d, screen2d, + lookuptex2d, scale_factor, present_fb, blending, @@ -575,90 +604,93 @@ impl<'a> renderer::Renderer<'a> for Renderer { None }; + let mut builder = self.ctx.new_pipeline_gate(); let v = session .views .active() .expect("there must always be an active view"); - let v_data = view_data.get(&v.id).unwrap(); - let l_data = &v_data.layer; let view_ortho = Matrix4::ortho(v.width(), v.fh, Origin::TopLeft); - let mut builder = self.ctx.new_pipeline_gate(); - - // Render to view staging buffer. - builder.pipeline::( - &v_data.staging_fb, - pipeline_st, - |pipeline, mut shd_gate| { - // Render staged brush strokes. - if let Some(tess) = staging_tess { - shd_gate.shade(shape2d, |mut iface, uni, mut rdr_gate| { - iface.set(&uni.ortho, view_ortho.into()); - iface.set(&uni.transform, identity); + { + let v_data = view_data.get(&v.id).unwrap().borrow(); + let l_data = &v_data.layer; + + // Render to view staging buffer. + builder.pipeline::( + &v_data.staging_fb, + pipeline_st, + |pipeline, mut shd_gate| { + // Render staged brush strokes. + if let Some(tess) = staging_tess { + shd_gate.shade(shape2d, |mut iface, uni, mut rdr_gate| { + iface.set(&uni.ortho, view_ortho.into()); + iface.set(&uni.transform, identity); + + rdr_gate.render(render_st, |mut tess_gate| tess_gate.render(&tess)) + })?; + } + // Render staging paste buffer. + if let Some(tess) = paste_tess { + let bound_paste = pipeline + .bind_texture(paste) + .expect("binding textures never fails. qed."); + shd_gate.shade(sprite2d, |mut iface, uni, mut rdr_gate| { + iface.set(&uni.ortho, view_ortho.into()); + iface.set(&uni.transform, identity); + iface.set(&uni.tex, bound_paste.binding()); - rdr_gate.render(render_st, |mut tess_gate| tess_gate.render(&tess)) - })?; - } - // Render staging paste buffer. - if let Some(tess) = paste_tess { + rdr_gate.render(render_st, |mut tess_gate| tess_gate.render(&tess)) + })?; + } + Ok(()) + }, + ); + + // Render to view final buffer. + builder.pipeline::( + &l_data.fb, + &pipeline_st.clone().enable_clear_color(false), + |pipeline, mut shd_gate| { let bound_paste = pipeline .bind_texture(paste) .expect("binding textures never fails. qed."); - shd_gate.shade(sprite2d, |mut iface, uni, mut rdr_gate| { - iface.set(&uni.ortho, view_ortho.into()); - iface.set(&uni.transform, identity); - iface.set(&uni.tex, bound_paste.binding()); - - rdr_gate.render(render_st, |mut tess_gate| tess_gate.render(&tess)) - })?; - } - Ok(()) - }, - ); - - // Render to view final buffer. - builder.pipeline::( - &l_data.fb, - &pipeline_st.clone().enable_clear_color(false), - |pipeline, mut shd_gate| { - let bound_paste = pipeline - .bind_texture(paste) - .expect("binding textures never fails. qed."); - - // Render final brush strokes. - if let Some(tess) = final_tess { - shd_gate.shade(shape2d, |mut iface, uni, mut rdr_gate| { - iface.set(&uni.ortho, view_ortho.into()); - iface.set(&uni.transform, identity); - let render_st = if blending == &Blending::Constant { - render_st.clone().set_blending(blending::Blending { - equation: Equation::Additive, - src: Factor::One, - dst: Factor::Zero, - }) - } else { - render_st.clone() - }; - - rdr_gate.render(&render_st, |mut tess_gate| tess_gate.render(&tess)) - })?; - } - if !paste_outputs.is_empty() { - shd_gate.shade(sprite2d, |mut iface, uni, mut rdr_gate| { - iface.set(&uni.ortho, view_ortho.into()); - iface.set(&uni.transform, identity); - iface.set(&uni.tex, bound_paste.binding()); + // Render final brush strokes. + if let Some(tess) = final_tess { + shd_gate.shade(shape2d, |mut iface, uni, mut rdr_gate| { + iface.set(&uni.ortho, view_ortho.into()); + iface.set(&uni.transform, identity); + + let render_st = if blending == &Blending::Constant { + render_st.clone().set_blending(blending::Blending { + equation: Equation::Additive, + src: Factor::One, + dst: Factor::Zero, + }) + } else { + render_st.clone() + }; + + rdr_gate.render(&render_st, |mut tess_gate| tess_gate.render(&tess)) + })?; + } + if !paste_outputs.is_empty() { + shd_gate.shade(sprite2d, |mut iface, uni, mut rdr_gate| { + iface.set(&uni.ortho, view_ortho.into()); + iface.set(&uni.transform, identity); + iface.set(&uni.tex, bound_paste.binding()); - for out in paste_outputs.drain(..) { - rdr_gate.render(render_st, |mut tess_gate| tess_gate.render(&out))?; - } - Ok(()) - })?; - } - Ok(()) - }, - ); + for out in paste_outputs.drain(..) { + rdr_gate + .render(render_st, |mut tess_gate| tess_gate.render(&out))?; + } + Ok(()) + })?; + } + Ok(()) + }, + ); + } // Render to screen framebuffer. let bg = Rgba::from(session.settings["background"].to_rgba8()); @@ -684,7 +716,8 @@ impl<'a> renderer::Renderer<'a> for Renderer { })?; } - for (id, v) in view_data.iter_mut() { + for (id, rcv) in view_data.iter() { + let mut v = rcv.borrow_mut(); if let Some(view) = session.views.get(*id) { let transform = Matrix4::from_translation( @@ -736,7 +769,8 @@ impl<'a> renderer::Renderer<'a> for Renderer { // Render view animations. if session.settings["animation"].is_set() { - for (id, v) in view_data.iter_mut() { + for (id, v) in view_data.iter() { + let v = &mut *v.borrow_mut(); match (&v.anim_tess, session.views.get(*id)) { (Some(tess), Some(view)) if view.animation.len() > 1 => { let bound_layer = pipeline @@ -780,6 +814,48 @@ impl<'a> renderer::Renderer<'a> for Renderer { Ok(()) })?; + shd_gate.shade(lookuptex2d, |mut iface, uni, mut rdr_gate| { + iface.set(&uni.ortho, ortho); + if session.settings["animation"].is_set() { + for (id, v) in view_data.iter() { + let v = &mut *v.borrow_mut(); + match (&v.anim_lt_tess, session.views.get(*id)) { + (Some(tess), Some(view)) if !view.lookuptexture().is_none() => { + let ltid = view.lookuptexture().unwrap(); + match ( + view_data.get(<id), + session.views.get(ltid), + ) { + (Some(rcltv), Some(ltview)) => { + let mut ltv = rcltv.borrow_mut(); + let bound_layer = pipeline + .bind_texture(v.layer.fb.color_slot()) + .expect("binding textures never fails"); + let lookup_layer = pipeline + .bind_texture(ltv.layer.fb.color_slot()) + .expect("binding textures never fails"); + let t = Matrix4::from_translation( + Vector2::new(0., view.zoom).extend(0.), + ); + // Render layer animation. + iface.set(&uni.tex, bound_layer.binding()); + iface.set(&uni.ltex, lookup_layer.binding()); + iface.set(&uni.ltexreg, [0.5, 1.]); + iface.set(&uni.transform, t.into()); + rdr_gate.render(render_st, |mut tess_gate| { + tess_gate.render(tess) + })?; + } + _ => (), + } + } + _ => (), + } + } + } + Ok(()) + })?; + // Render help. if let Some((win_tess, text_tess)) = help_tess { shd_gate.shade(shape2d, |_iface, _uni, mut rdr_gate| { @@ -859,7 +935,7 @@ impl<'a> renderer::Renderer<'a> for Renderer { let extent = v.extent(); if let Some(vr) = session.views.get_mut(id) { - let v_data = view_data.get_mut(&id).unwrap(); + let mut v_data = view_data.get(&id).unwrap().borrow_mut(); match state { ViewState::Dirty(_) if is_resized => { @@ -939,8 +1015,10 @@ impl Renderer { if let Some((s, pixels)) = session.views.get_snapshot_safe(id) { let (w, h) = (s.width(), s.height()); - self.view_data - .insert(id, ViewData::new(w, h, Some(pixels), &mut self.ctx)); + self.view_data.insert( + id, + RefCell::new(ViewData::new(w, h, Some(pixels), &mut self.ctx)), + ); } } Effect::ViewRemoved(id) => { @@ -983,10 +1061,11 @@ impl Renderer { self.resize_view(v, *w, *h)?; } ViewOp::Clear(color) => { - let view = self + let mut view = self .view_data - .get_mut(&v.id) - .expect("views must have associated view data"); + .get(&v.id) + .expect("views must have associated view data") + .borrow_mut(); view.layer .fb @@ -995,10 +1074,11 @@ impl Renderer { .map_err(Error::Texture)?; } ViewOp::Blit(src, dst) => { - let view = self + let mut view = self .view_data - .get_mut(&v.id) - .expect("views must have associated view data"); + .get(&v.id) + .expect("views must have associated view data") + .borrow_mut(); let (_, texels) = v.layer.get_snapshot_rect(&src.map(|n| n as i32)).unwrap(); // TODO: Handle this nicely? let texels = util::align_u8(&texels); @@ -1085,8 +1165,9 @@ impl Renderer { ViewOp::SetPixel(rgba, x, y) => { let fb = &mut self .view_data - .get_mut(&v.id) + .get(&v.id) .expect("views must have associated view data") + .borrow_mut() .layer .fb; let texels = &[*rgba]; @@ -1095,6 +1176,55 @@ impl Renderer { .upload_part_raw(GenMipmaps::No, [*x as u32, *y as u32], [1, 1], texels) .map_err(Error::Texture)?; } + //TODO: could be more generic + ViewOp::GenerateLookupTextureIR(src, dst) => { + let mut view = self + .view_data + .get(&v.id) + .expect("views must have associated view data") + .borrow_mut(); + + let (_, pixels) = v.layer.get_snapshot_rect(&src.map(|n| n as i32)).unwrap(); // TODO: Handle this nicely? + + let modified: Vec = pixels + .iter() + .enumerate() + .map(|(i, p)| { + if p.a == 0 { + return Rgba8 { + r: 0, + g: 0, + b: 0, + a: 0, + }; + } + + let x = i as f32 % src.width(); + let xf = 256.0 / src.width(); + let y = (i as f32 / src.width()).floor(); + let yf = 256.0 / src.height(); + Rgba8 { + r: (x * xf).floor() as u8, + g: (y * yf).floor() as u8, + b: 0, + a: p.a, + } + }) + .collect(); + + let modified = util::align_u8(&modified); + + view.layer + .fb + .color_slot() + .upload_part_raw( + GenMipmaps::No, + [dst.x1 as u32, dst.y1 as u32], + [src.width() as u32, src.height() as u32], + modified, + ) + .map_err(Error::Texture)?; + } } } Ok(()) @@ -1103,8 +1233,9 @@ impl Renderer { fn handle_view_damaged(&mut self, view: &View) -> Result<(), RendererError> { let layer = &mut self .view_data - .get_mut(&view.id) + .get(&view.id) .expect("views must have associated view data") + .borrow_mut() .layer; let (_, pixels) = view.layer.current_snapshot(); @@ -1156,7 +1287,7 @@ impl Renderer { l.upload_part([0, vh - th], [tw, th], texels)?; } - self.view_data.insert(view.id, view_data); + self.view_data.insert(view.id, RefCell::new(view_data)); Ok(()) } @@ -1167,16 +1298,29 @@ impl Renderer { } // TODO: Does this need to run if the view has only one frame? for v in s.views.iter() { + if v.is_lookuptexture() { + continue; + } // FIXME: When `v.animation.val()` doesn't change, we don't need // to re-create the buffer. let batch = draw::draw_view_animation(s, v); - - if let Some(vd) = self.view_data.get_mut(&v.id) { - vd.anim_tess = Some( + if let Some(vd) = self.view_data.get(&v.id) { + vd.borrow_mut().anim_tess = Some( self.ctx .tessellation::<_, Sprite2dVertex>(batch.vertices().as_slice()), ); } + + // lookup-texture animation enabled + if let Some(_) = v.lookuptexture() { + let ltbatch = draw::draw_view_lookuptexture_animation(s, v); + if let Some(vd) = self.view_data.get(&v.id) { + vd.borrow_mut().anim_lt_tess = Some( + self.ctx + .tessellation::<_, Sprite2dVertex>(ltbatch.vertices().as_slice()), + ); + } + } } } @@ -1184,8 +1328,8 @@ impl Renderer { for v in s.views.iter() { let batch = draw::draw_view_composites(s, v); - if let Some(vd) = self.view_data.get_mut(&v.id) { - vd.layer_tess = Some( + if let Some(vd) = self.view_data.get(&v.id) { + vd.borrow_mut().layer_tess = Some( self.ctx .tessellation::<_, Sprite2dVertex>(batch.vertices().as_slice()), ); diff --git a/src/session.rs b/src/session.rs index a9da6135..81f4ba1e 100644 --- a/src/session.rs +++ b/src/session.rs @@ -3006,6 +3006,34 @@ impl Session { v.paint_color(*color, x, y); } } + Command::LookupTextureOn => { + if !self.active_view().is_lookuptexture() { + self.active_view_mut().lookuptexture_on(); + } else { + self.message( + format!("View is already set as a lookup texture"), + MessageType::Error, + ); + } + } + Command::LookupTextureSet(d) => { + if d == 0 { + self.message( + format!("Cannot set a view as its own lookup texture"), + MessageType::Error, + ); + return; + } + + let current = self.views.active_id; + if let Some(id) = self.views.relativen(current, d) { + self.active_view_mut().lookuptexture_set(id); + if !self.view(id).is_lookuptexture() { + self.activate(id); // TODO: seems to crash on first draw without it + self.view_mut(id).lookuptexture_on(); + } + } + } }; } diff --git a/src/view.rs b/src/view.rs index 9bb93277..5fe27ea8 100644 --- a/src/view.rs +++ b/src/view.rs @@ -107,6 +107,8 @@ pub enum ViewOp { Resize(u32, u32), /// Paint a single pixel. SetPixel(Rgba8, i32, i32), + /// Generate initial lookup texture map for given area. + GenerateLookupTextureIR(Rect, Rect), } /// A view on a sprite or image. @@ -139,6 +141,9 @@ pub struct View { /// Which view snapshot has been saved to disk, if any. saved_snapshot: Option, + /// Which other view is current one's lookup texture + lookuptexture: Option, + lookuptexture_on: bool } /// View animation. @@ -207,6 +212,8 @@ impl View { animation: Animation::new(frames), state: ViewState::Okay, saved_snapshot, + lookuptexture: None, + lookuptexture_on: false, resource, } } @@ -411,6 +418,16 @@ impl View { self.state == ViewState::Okay } + /// Check whether the view is configured as a lookup texture. + pub fn is_lookuptexture(&self) -> bool { + self.lookuptexture_on + } + + /// Return the set lookup texture if exists. + pub fn lookuptexture(&self) -> Option { + self.lookuptexture + } + /// Return the file status as a string. pub fn status(&self) -> String { self.file_status.to_string() @@ -431,6 +448,28 @@ impl View { Rect::origin(self.width() as i32, self.fh as i32) } + /// Set another view as current view's lookup texture + pub fn lookuptexture_set(&mut self, ltid: ViewId) { + assert!(self.id != ltid, "cannot set a view as its own lookup texture"); + self.lookuptexture = Some(ltid); + } + + /// Set current view as a lookup texture + pub fn lookuptexture_on(&mut self) { + assert!(self.animation.len() <= 1, "view is already an animation, cannot transform to lookup texture"); + self.lookuptexture_on = true; + let width = self.width() as f32; + let (fw, fh) = (self.fw as f32, self.fh as f32); + + self.extend(); + // build initial intermediate map + self.ops.push(ViewOp::GenerateLookupTextureIR( + Rect::new(0., 0., fw as f32, fh), + Rect::new(width, 0., width + fw, fh), + )); + + } + //////////////////////////////////////////////////////////////////////////// fn resized(&mut self) { @@ -767,11 +806,32 @@ impl ViewManager { self.range(id..).nth(1) } + /// Get nth `ViewId` *after* given id. + pub fn aftern(&self, id: ViewId, n: usize) -> Option { + self.range(id..).nth(n) + } + /// Get `ViewId` *before* given id. pub fn before(&self, id: ViewId) -> Option { self.range(..id).next_back() } + /// Get nth `ViewId` *before* given id. + pub fn beforen(&self, id: ViewId, n: usize) -> Option { + self.range(..id).nth_back(n) + } + + /// Get nth `ViewId` relative (before or after) to given id + pub fn relativen(&self, id: ViewId, n: i32) -> Option { + if n > 0 { + self.aftern(id, n as usize) + } else if n < 0 { + self.beforen(id, n.abs() as usize - 1) + } else { // n == 0 + Some(id) + } + } + /// Get the first view. pub fn first(&self) -> Option<&View> { self.iter().next() From dec8d05487dd97c86d3c6d756d95f9839cf2cb28 Mon Sep 17 00:00:00 2001 From: Guy Sviry Date: Sat, 27 Jan 2024 11:36:25 +0200 Subject: [PATCH 2/4] Add "LUT" view indication --- src/cmd.rs | 6 ++++-- src/draw.rs | 13 +++++++++++- src/font.rs | 6 ++++-- src/session.rs | 10 ++++------ src/view.rs | 54 ++++++++++++++++++++++++++++++-------------------- 5 files changed, 56 insertions(+), 33 deletions(-) diff --git a/src/cmd.rs b/src/cmd.rs index 0b5bd850..a54f6c7e 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -122,7 +122,7 @@ pub enum Command { ViewNext, ViewPrev, - LookupTextureOn, + LookupTextureMode(bool), LookupTextureSet(i32), Noop, @@ -1055,7 +1055,9 @@ impl Default for Commands { .map(|((_, i), (x, y))| Command::PaintPalette(i, x, y)) }) .command("lt/on", "Set current view as lookup texture", |p| { - p.value(Command::LookupTextureOn) }) + p.value(Command::LookupTextureMode(true)) }) + .command("lt/off", "Revert view to normal mode", |p| { + p.value(Command::LookupTextureMode(false)) }) .command("lt/set", "Set another view as lookup texture to current view", |p| { p.then(integer::().label("")) .map(|(_, d)| Command::LookupTextureSet(d)) diff --git a/src/draw.rs b/src/draw.rs index abe263ca..a198ca0c 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -254,7 +254,7 @@ fn draw_ui(session: &Session, canvas: &mut shape2d::Batch, text: &mut TextBatch) if session.settings["ui/view-info"].is_set() { // View info - text.add( + let x = text.add( &format!("{}x{}x{}", v.fw, v.fh, v.animation.len()), offset.x, offset.y - self::LINE_HEIGHT, @@ -262,6 +262,17 @@ fn draw_ui(session: &Session, canvas: &mut shape2d::Batch, text: &mut TextBatch) color::GREY, TextAlign::Left, ); + + if v.is_lookuptexture() { + text.add( + &format!(" LUT"), + x, + offset.y - self::LINE_HEIGHT, + self::TEXT_LAYER, + color::GREEN, + TextAlign::Left, + ); + } } } if session.settings["ui/status"].is_set() { diff --git a/src/font.rs b/src/font.rs index 945b0cdb..07517cbc 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,4 +1,4 @@ -use crate::gfx::sprite2d; +use crate::gfx::{sprite2d}; use crate::gfx::{Rect, Repeat, Rgba8, ZDepth}; pub enum TextAlign { @@ -27,7 +27,7 @@ impl TextBatch { z: ZDepth, color: Rgba8, align: TextAlign, - ) { + ) -> f32 { let offset: usize = 32; let gw = self.gw; @@ -56,6 +56,8 @@ impl TextBatch { ); sx += gw; } + + return sx; } pub fn offset(&mut self, x: f32, y: f32) { diff --git a/src/session.rs b/src/session.rs index 81f4ba1e..8c1a4a3e 100644 --- a/src/session.rs +++ b/src/session.rs @@ -3006,14 +3006,11 @@ impl Session { v.paint_color(*color, x, y); } } - Command::LookupTextureOn => { - if !self.active_view().is_lookuptexture() { + Command::LookupTextureMode(on) => { + if on { self.active_view_mut().lookuptexture_on(); } else { - self.message( - format!("View is already set as a lookup texture"), - MessageType::Error, - ); + self.active_view_mut().lookuptexture_off(); } } Command::LookupTextureSet(d) => { @@ -3031,6 +3028,7 @@ impl Session { if !self.view(id).is_lookuptexture() { self.activate(id); // TODO: seems to crash on first draw without it self.view_mut(id).lookuptexture_on(); + self.activate(current); } } } diff --git a/src/view.rs b/src/view.rs index 5fe27ea8..e28b45f8 100644 --- a/src/view.rs +++ b/src/view.rs @@ -448,28 +448,6 @@ impl View { Rect::origin(self.width() as i32, self.fh as i32) } - /// Set another view as current view's lookup texture - pub fn lookuptexture_set(&mut self, ltid: ViewId) { - assert!(self.id != ltid, "cannot set a view as its own lookup texture"); - self.lookuptexture = Some(ltid); - } - - /// Set current view as a lookup texture - pub fn lookuptexture_on(&mut self) { - assert!(self.animation.len() <= 1, "view is already an animation, cannot transform to lookup texture"); - self.lookuptexture_on = true; - let width = self.width() as f32; - let (fw, fh) = (self.fw as f32, self.fh as f32); - - self.extend(); - // build initial intermediate map - self.ops.push(ViewOp::GenerateLookupTextureIR( - Rect::new(0., 0., fw as f32, fh), - Rect::new(width, 0., width + fw, fh), - )); - - } - //////////////////////////////////////////////////////////////////////////// fn resized(&mut self) { @@ -593,6 +571,38 @@ impl View { Ok(e_id) } + + /// Set another view as current view's lookup texture + pub fn lookuptexture_set(&mut self, ltid: ViewId) { + assert!(self.id != ltid, "cannot set a view as its own lookup texture"); + self.lookuptexture = Some(ltid); + } + + /// Set current view as a lookup texture + pub fn lookuptexture_on(&mut self) { + self.lookuptexture_on = true; + + if self.animation.len() == 1 { + let width = self.width() as f32; + let (fw, fh) = (self.fw as f32, self.fh as f32); + + self.extend(); + // build initial intermediate map + self.ops.push(ViewOp::GenerateLookupTextureIR( + Rect::new(0., 0., fw as f32, fh), + Rect::new(width, 0., width + fw, fh), + )); + } + } + + /// Set current view as a lookup texture + pub fn lookuptexture_off(&mut self) { + self.lookuptexture_on = false; + + if self.animation.len() > 1 { + self.shrink(); + } + } } /////////////////////////////////////////////////////////////////////////////// From 39cc0c41ec2f2ee526a7371eaa9106e561bb28f9 Mon Sep 17 00:00:00 2001 From: Guy Sviry Date: Wed, 31 Jan 2024 22:20:06 +0200 Subject: [PATCH 3/4] Fix shader bug where Add vec2(1/512) to lookup uv calculated from texture to ensure we land mid-pixel when sampling the final texture --- src/gl/data/lookuptex.frag | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gl/data/lookuptex.frag b/src/gl/data/lookuptex.frag index df1aa790..b5e77cf4 100644 --- a/src/gl/data/lookuptex.frag +++ b/src/gl/data/lookuptex.frag @@ -22,6 +22,7 @@ void main() { vec4 texel = texture(tex, f_uv); texel = vec4(linearTosRGB(texel.rgb), texel.a); // Convert to linear space texel.rg = texel.rg * ltexreg; + texel.rg = texel.rg + vec2(0.001953125, 0.001953125); if (texel.a > 0.0) { // Non-transparent pixel vec4 lt_texel = texture(ltex, texel.rg); fragColor = vec4( From 72ba5c1e12426e592d26258f7198d53599175d25 Mon Sep 17 00:00:00 2001 From: Guy Sviry Date: Wed, 31 Jan 2024 22:23:03 +0200 Subject: [PATCH 4/4] Add lookup texture sample mode --- .gitignore | 1 + assets/cursors.png | Bin 731 -> 855 bytes config/init.rx | 1 + src/cmd.rs | 7 +++ src/draw.rs | 31 ++++++++- src/font.rs | 2 +- src/gl/mod.rs | 49 +++++---------- src/session.rs | 145 ++++++++++++++++++++++++++++++++++++++++++- src/view/resource.rs | 9 +++ 9 files changed, 210 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 3e45d080..6d828d26 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ flamegraph.svg **/*.rs.bk *.AppImage AppDir +.vscode diff --git a/assets/cursors.png b/assets/cursors.png index 860df2d5367882866cd76f24a2fc59581a07b9f1..c3c07df746ad1b3adbebccdce95f56d2c093f0bf 100644 GIT binary patch literal 855 zcmV-d1E~CoP);420SN>HFcbrTK!QC$umVCXQl8hT)6`AcFa54}r+Xy)7sqz$7W_T;hZZB^)zRoW zmj)9p$g{ViOcUCIta{(_s&XdZM1d~HnA1F~1#>dZG&olB&n7ITNyuYLjm(f~Rt1o4 zraTiZsG+y#9NdLpfsA!uT~S9k(s~wl2aR{%$csDIjP+}b3zkO{>R1y4KRAl2Z(6Ky+=fv-qnBHjO0nu=`&Y&8?UX&ev hJr(>-k*D-V<3H>U9xwf<`Q!iq002ovPDHLkV1g}kq#6JK literal 731 zcmV<10wn#3P)AHIP0007_NklXaOM_G4IWKZSQ(*$Idwy@ubX`XLe@CmrJgf z1H60tru+2e8`kAe{T{kk@4M@-4@diS{K(Pz)7d?k%m4WK>*acl(hz5nm!CDEJ#>I# z$S0U}fi%F#Y=&82gUthLSVG4FHuGxo(reA5EruCsjbzBOec_x)YCiU>{h)9fw=}XXmuv`eGO6K+J&mac3a15&@4o-|d-?t6Ctks3?JYRz z1;}93klXyw5N^gBq-H<~l4fJsU`XNAxcvO7fBE9|^d+p;9p$0RUdiLWY)so=*vT73a7zX4U?C!Rkxd!r8Zf}=e}uVFS}-|A;c2o#nMrs32i}U z>LZp+eK7_hWH1_7>2r0B@fkFWT$(hW3n{alyi7H2ppPo3pX;k}Gn}4MJbukqLx?rV zJ+%CcrFGmGjG}=R@r+^zzzQ^mXSbxW%vGv5p}zrS zwfjy>&FTG7RCr3nNwzUo^3SGP>LOJhQ#vw3rdbUjm+A9DEWsJxeaOKr{03yq`&wdL zEfaeb6FebeB-#)w`S+q)b_*tT?1VGxFKN++5G}~_?z88FoAI7FoD^ijRQ;BQQS?|D zK1nvhO8#TuD9<{4&I4Y6(8cjbY-z04q75N#Of;~MJ!&lzdlX<{*d(|xtmHoyj`O_T zXXVA$nnz2}$!jERso&BN;v`~-+$(EP;1+%oU{U^KgE_?SN?v-_gtp)YUS*jlx?ep+ z?#FoZ7eH(X%3wSPb}T=`b?W;k`a6cWBMvvrpkY9?{}=L|YM$L4{{d0e;2_y~dpQ6A N002ovPDHLkV1hg0WRCy< diff --git a/config/init.rx b/config/init.rx index 3931b104..764d6a38 100644 --- a/config/init.rx +++ b/config/init.rx @@ -23,6 +23,7 @@ map r :redo -- Redo act map x :swap -- Swap foreground/background colors map/normal b :brush -- Reset brush map/normal g :flood -- Flood tool +map/normal a :lt/sampler {:tool/prev} -- Lookup-texture sample tool map/normal e :brush/set erase {:brush/unset erase} -- Erase (hold) map/normal :brush/set multi {:brush/unset multi} -- Multi-brush (hold) map/normal = :brush/toggle perfect -- Pixel-perfect brush diff --git a/src/cmd.rs b/src/cmd.rs index a54f6c7e..4782a98a 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -122,6 +122,7 @@ pub enum Command { ViewNext, ViewPrev, + LookupTextureSample, LookupTextureMode(bool), LookupTextureSet(i32), @@ -1062,6 +1063,12 @@ impl Default for Commands { p.then(integer::().label("")) .map(|(_, d)| Command::LookupTextureSet(d)) }) + .command("lt/sample", "Search lookup texture for matching pixels", |p| { + p.value(Command::LookupTextureSample) + }) + .command("lt/sampler", "Switch to lookup texture sampler tool", |p| { + p.value(Command::Tool(Tool::LookupTextureSampler)) + }) } } diff --git a/src/draw.rs b/src/draw.rs index a198ca0c..cdf7b41b 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -69,6 +69,7 @@ pub mod cursors { const PAN: Cursor = Cursor::new(Rect::new(48., 0., 64., 16.), -8., -8., false); const ERASE: Cursor = Cursor::new(Rect::new(64., 0., 80., 16.), -8., -8., true); const FLOOD: Cursor = Cursor::new(Rect::new(80., 0., 96., 16.), -8., -8., false); + const LOOKUP: Cursor = Cursor::new(Rect::new(96., 0., 112., 16.), -5.5, -8., false); pub fn info(t: &Tool, b: &Brush, m: Mode, in_view: bool, in_selection: bool) -> Option { match (m, t) { @@ -82,6 +83,7 @@ pub mod cursors { Tool::Sampler => self::SAMPLER, Tool::Pan(_) => self::PAN, Tool::FloodFill => self::FLOOD, + Tool::LookupTextureSampler => self::LOOKUP, Tool::Brush => match m { Mode::Visual(_) if in_selection && in_view => self::OMNI, @@ -150,7 +152,7 @@ impl Context { fn draw_ui(session: &Session, canvas: &mut shape2d::Batch, text: &mut TextBatch) { let view = session.active_view(); - if let Some(selection) = session.selection { + if let Some(selection) = &session.selection { let fill = match session.mode { Mode::Visual(VisualState::Selecting { .. }) => { Rgba8::new(color::RED.r, color::RED.g, color::RED.b, 0x55) @@ -212,6 +214,33 @@ fn draw_ui(session: &Session, canvas: &mut shape2d::Batch, text: &mut TextBatch) } } + if let (Some(lss), Some(ltid)) = (&session.lookup_sample_state, view.lookuptexture()) { + match session.mode { + Mode::Visual(VisualState::LookupSampling) => { + let ltv = session.view(ltid); + + let offset = session.offset + ltv.offset; + let t = Matrix4::from_translation(offset.extend(0.)) * Matrix4::from_scale(ltv.zoom); + + for (i, c) in lss.candidates.iter().enumerate() { + let mut stroke = color::RED; + if i as i32 == lss.selected { + stroke = color::GREEN; + } + + canvas.add(Shape::Rectangle( + c.0.transform(t), + self::UI_LAYER, + Rotation::ZERO, + Stroke::new(1., stroke.into()), + Fill::Empty, + )); + } + } + _ => () + }; + } + for v in session.views.iter() { let offset = v.offset + session.offset; diff --git a/src/font.rs b/src/font.rs index 07517cbc..c64708d8 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,4 +1,4 @@ -use crate::gfx::{sprite2d}; +use crate::gfx::sprite2d; use crate::gfx::{Rect, Repeat, Rgba8, ZDepth}; pub enum TextAlign { diff --git a/src/gl/mod.rs b/src/gl/mod.rs index 26d949b1..ef4faa88 100644 --- a/src/gl/mod.rs +++ b/src/gl/mod.rs @@ -822,11 +822,8 @@ impl<'a> renderer::Renderer<'a> for Renderer { match (&v.anim_lt_tess, session.views.get(*id)) { (Some(tess), Some(view)) if !view.lookuptexture().is_none() => { let ltid = view.lookuptexture().unwrap(); - match ( - view_data.get(<id), - session.views.get(ltid), - ) { - (Some(rcltv), Some(ltview)) => { + match view_data.get(<id) { + Some(rcltv) => { let mut ltv = rcltv.borrow_mut(); let bound_layer = pipeline .bind_texture(v.layer.fb.color_slot()) @@ -1083,16 +1080,11 @@ impl Renderer { let (_, texels) = v.layer.get_snapshot_rect(&src.map(|n| n as i32)).unwrap(); // TODO: Handle this nicely? let texels = util::align_u8(&texels); - view.layer - .fb - .color_slot() - .upload_part_raw( - GenMipmaps::No, - [dst.x1 as u32, dst.y1 as u32], - [src.width() as u32, src.height() as u32], - texels, - ) - .map_err(Error::Texture)?; + view.layer.upload_part( + [dst.x1 as u32, dst.y1 as u32], + [src.width() as u32, src.height() as u32], + texels, + )?; } ViewOp::Yank(src) => { let (_, pixels) = v.layer.get_snapshot_rect(&src.map(|n| n)).unwrap(); @@ -1163,18 +1155,15 @@ impl Renderer { ); } ViewOp::SetPixel(rgba, x, y) => { - let fb = &mut self + let layer = &mut self .view_data .get(&v.id) .expect("views must have associated view data") .borrow_mut() - .layer - .fb; + .layer; let texels = &[*rgba]; let texels = util::align_u8(texels); - fb.color_slot() - .upload_part_raw(GenMipmaps::No, [*x as u32, *y as u32], [1, 1], texels) - .map_err(Error::Texture)?; + layer.upload_part([*x as u32, *y as u32], [1, 1], texels)?; } //TODO: could be more generic ViewOp::GenerateLookupTextureIR(src, dst) => { @@ -1203,10 +1192,11 @@ impl Renderer { let xf = 256.0 / src.width(); let y = (i as f32 / src.width()).floor(); let yf = 256.0 / src.height(); + Rgba8 { r: (x * xf).floor() as u8, g: (y * yf).floor() as u8, - b: 0, + b: p.r.max(p.g).max(p.b), a: p.a, } }) @@ -1214,16 +1204,11 @@ impl Renderer { let modified = util::align_u8(&modified); - view.layer - .fb - .color_slot() - .upload_part_raw( - GenMipmaps::No, - [dst.x1 as u32, dst.y1 as u32], - [src.width() as u32, src.height() as u32], - modified, - ) - .map_err(Error::Texture)?; + view.layer.upload_part( + [dst.x1 as u32, dst.y1 as u32], + [src.width() as u32, src.height() as u32], + modified, + )?; } } } diff --git a/src/session.rs b/src/session.rs index 8c1a4a3e..09b21ce8 100644 --- a/src/session.rs +++ b/src/session.rs @@ -91,6 +91,7 @@ impl fmt::Display for Mode { Self::Visual(VisualState::Selecting { dragging: true }) => "visual (dragging)".fmt(f), Self::Visual(VisualState::Selecting { .. }) => "visual".fmt(f), Self::Visual(VisualState::Pasting) => "visual (pasting)".fmt(f), + Self::Visual(VisualState::LookupSampling) => "visual (LUT sampling)".fmt(f), Self::Command => "command".fmt(f), Self::Present => "present".fmt(f), Self::Help => "help".fmt(f), @@ -102,6 +103,7 @@ impl fmt::Display for Mode { pub enum VisualState { Selecting { dragging: bool }, Pasting, + LookupSampling, } impl VisualState { @@ -162,6 +164,30 @@ impl Deref for Selection { } } +#[derive(Clone)] +pub struct LookupSampleCandidate(pub Rect, pub Rgba8); + +#[derive(Clone)] +pub struct LookupSampleState { + pub candidates: Vec, + pub selected: i32, + pub color: Rgba8, +} + +impl LookupSampleState { + fn new(color: Rgba8) -> LookupSampleState { + LookupSampleState { + candidates: Vec::new(), + selected: -1, + color: color + } + } + + fn next(&mut self, i: i32) { + self.selected = (self.selected + i) % self.candidates.len() as i32; + } +} + /// Session effects. Eg. view creation/destruction. /// Anything the renderer might want to know. #[derive(Clone, Debug)] @@ -233,6 +259,8 @@ pub enum Tool { Sampler, /// Used to pan the workspace. Pan(PanState), + /// Used to sample colors from lookup texture + LookupTextureSampler, } #[derive(PartialEq, Eq, Debug, Clone, Copy)] @@ -614,6 +642,10 @@ pub struct Session { /// Current pixel selection. pub selection: Option, + /// Current pixel selection. + pub lookup_sample_state: Option, + pub prev_lookup_sample_state: Option, + /// The session's current settings. pub settings: Settings, /// Settings recently changed. @@ -737,6 +769,8 @@ impl Session { mode: Mode::Normal, prev_mode: Option::default(), selection: Option::default(), + lookup_sample_state: Option::default(), + prev_lookup_sample_state: Option::default(), message: Message::default(), avg_time: time::Duration::from_secs(0), frame_number: 0, @@ -1178,6 +1212,11 @@ impl Session { Mode::Command => { self.cmdline.clear(); } + Mode::Visual(VisualState::LookupSampling) => { + if self.lookup_sample_state.is_some() { + self.prev_lookup_sample_state = std::mem::replace(&mut self.lookup_sample_state, None); + } + } _ => {} } @@ -1895,6 +1934,10 @@ impl Session { } debug!("flood fill in: {:?}", start_time.elapsed()); } + Tool::LookupTextureSampler => { + self.command(Command::LookupTextureSample); + self.prev_tool(); + } }, Mode::Command => { // TODO @@ -1918,6 +1961,9 @@ impl Session { self.center_selection(self.cursor); self.command(Command::SelectionPaste); } + Mode::Visual(VisualState::LookupSampling) => { + // TODO + } Mode::Present | Mode::Help => {} } } else { @@ -2100,6 +2146,37 @@ impl Session { return; } } + Mode::Visual(VisualState::LookupSampling) => { + if state == InputState::Pressed { + match key { + platform::Key::Escape => { + self.switch_mode(Mode::Normal); + return; + } + platform::Key::Tab => { + if let Some(lss) = self.lookup_sample_state.as_mut() { + if modifiers.shift { + lss.next(-1); + } else { + lss.next(1); + } + } + return; + } + platform::Key::Return => { + if let Some(lss) = &self.lookup_sample_state { + if lss.selected >= 0 { + self.pick_color(lss.candidates[lss.selected as usize].1) + } + } + self.switch_mode(Mode::Normal); + self.tool(Tool::Brush); + return; + } + _ => {} + } + } + } Mode::Command => { if state == InputState::Pressed { match key { @@ -3028,10 +3105,76 @@ impl Session { if !self.view(id).is_lookuptexture() { self.activate(id); // TODO: seems to crash on first draw without it self.view_mut(id).lookuptexture_on(); - self.activate(current); } } } + Command::LookupTextureSample => { + let v = self.active_view(); + if v.lookuptexture().is_none() { + self.message( + format!("Current view has no lookup texture"), + MessageType::Error, + ); + return; + } + + let lutid = v.lookuptexture().unwrap(); + let lutv = self + .views + .get(lutid) + .expect(&format!("view #{} must exist", lutid)); + + let (_, pixels) = lutv.layer.current_snapshot(); + + let hover = self.hover_color; + if hover.is_none() { + self.message(format!("Not hovering on any color"), MessageType::Error); + return; + } + + let hover = hover.unwrap(); + + let mut lss = LookupSampleState::new(hover); + for (i, pixel) in pixels.iter().cloned().enumerate() { + let vw = lutv.extent.fw * lutv.extent.nframes as u32; + let x = (i as u32 % vw) as f32; + let r = (i as u32 % vw) as f32; + let rf = 256.0 / (lutv.extent.fw as f32); + let y = (lutv.extent.fh - 1 - i as u32 / vw) as f32; + let g = (i as f32 / (vw as f32)).floor(); + let gf = 256.0 / (lutv.extent.fh as f32); + if pixel == hover { + // register potential candidate + lss.candidates.push(LookupSampleCandidate( + Rect::new(x, y, x + 1., y + 1.), + Rgba8::new( + (r * rf).floor() as u8, + (g * gf).floor() as u8, + pixel.r.max(pixel.g).max(pixel.b) as u8, + 255, + ), + )) + } + } + + if let Some(prevlss) = &self.prev_lookup_sample_state { + if prevlss.color == lss.color && + (prevlss.selected as usize) < lss.candidates.len() { + lss.selected = prevlss.selected; + } + } + + + if lss.candidates.len() > 0 { + self.lookup_sample_state = Some(lss); + self.switch_mode(Mode::Visual(VisualState::LookupSampling)); + } else { + self.message( + format!("No matching pixels in lookup texture"), + MessageType::Warning, + ); + } + } }; } diff --git a/src/view/resource.rs b/src/view/resource.rs index cd8e5010..2821cee6 100644 --- a/src/view/resource.rs +++ b/src/view/resource.rs @@ -307,6 +307,15 @@ impl LayerResource { if !(snapshot_rect.x1 <= rect.x1 && snapshot_rect.y1 <= rect.y1) || !(snapshot_rect.x2 >= rect.x2 && snapshot_rect.y2 >= rect.y2) { + debug!("snaption rect out of bounds: snaphot: ({},{}),({},{}) rect: ({},{}),({},{})", + snapshot_rect.x1, + snapshot_rect.y1, + snapshot_rect.x2, + snapshot_rect.y2, + rect.x1, + rect.y1, + rect.x2, + rect.y2); return None; } debug_assert!(w * h <= total_w * total_h);