blob: 1b0cbcc4ae6fb069a5547e4b61b496d55a5d2f2d [file] [log] [blame]
use super::ScreenBounds;
use crate::settings::{MotionFieldColoring, Settings};
use crate::views::{MIN_BLOCK_HEIGHT_FOR_TEXT, MIN_BLOCK_SIZE_TO_RENDER, MIN_BLOCK_WIDTH_FOR_TEXT};
use avm_stats::{CodingUnitKind, Frame, ProtoEnumMapping, Spatial, MOTION_VECTOR_PRECISION};
use egui::{pos2, vec2, Align2, Color32, Painter, Rect, Rounding, Shape, Stroke, TextStyle};
use itertools::Itertools;
fn is_rect_too_small(rect: Rect) -> bool {
rect.width() < MIN_BLOCK_SIZE_TO_RENDER || rect.height() < MIN_BLOCK_SIZE_TO_RENDER
}
fn is_text_too_small(rect: Rect) -> bool {
rect.width() < MIN_BLOCK_WIDTH_FOR_TEXT || rect.height() < MIN_BLOCK_HEIGHT_FOR_TEXT
}
// TODO(comc): Make configurable.
pub const REF_FRAME_COLORS: &[Color32] = &[
Color32::LIGHT_RED,
Color32::LIGHT_GREEN,
Color32::LIGHT_BLUE,
Color32::LIGHT_YELLOW,
Color32::BROWN,
Color32::KHAKI,
Color32::GOLD,
Color32::LIGHT_GRAY,
];
// TODO(comc): World space to screen space conversion could be done in a shader.
pub struct FrameOverlay<'a> {
frame: &'a Frame,
settings: &'a Settings,
}
impl<'a> FrameOverlay<'a> {
pub fn new(frame: &'a Frame, settings: &'a Settings) -> Self {
Self { frame, settings }
}
pub fn draw(&self, painter: &mut Painter) {
let view_settings = self.settings.sharable.view_mode.view_settings();
if self.settings.sharable.show_overlay {
if view_settings.show_transform_units {
self.draw_transform_units(painter);
}
if view_settings.show_coding_units {
self.draw_coding_units(painter);
}
if view_settings.show_superblocks {
self.draw_superblocks(painter);
}
if view_settings.show_prediction_modes {
self.draw_prediction_modes(painter);
}
if view_settings.show_transform_types {
self.draw_transform_modes(painter);
}
}
if (self.settings.sharable.show_overlay || self.settings.sharable.motion_field.show)
&& view_settings.show_motion_vectors
{
self.draw_motion_vectors(painter);
}
}
fn is_in_bounds(&self, world_rect: Rect, screen_bounds: Rect) -> bool {
let world_bounds = self.settings.sharable.world_bounds;
// Because the aspect ratio of the viewport is not necessarily the same as the current frame, we might see extra than the plain world_bounds.
let extended_world_bounds = world_bounds.screen_rect_to_world(screen_bounds, screen_bounds);
extended_world_bounds.intersects(world_rect)
}
fn draw_transform_units(&self, painter: &mut Painter) -> Option<()> {
let style = &self.settings.persistent.style.overlay;
let world_bounds = self.settings.sharable.world_bounds;
let clip_rect = painter.clip_rect();
let shapes = self
.frame
.iter_transform_rects(self.settings.sharable.current_plane.to_plane())
.filter_map(|world_rect| {
if !self.is_in_bounds(world_rect, clip_rect) {
return None;
}
let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
if is_rect_too_small(screen_rect) {
return None;
}
let points = [
screen_rect.left_top(),
screen_rect.right_top(),
screen_rect.right_bottom(),
screen_rect.left_bottom(),
];
Some(Shape::dashed_line(&points[..], style.transform_unit_stroke, 4.0, 4.0))
})
.flatten();
painter.extend(shapes);
None
}
fn draw_coding_units(&self, painter: &mut Painter) -> Option<()> {
let style = &self.settings.persistent.style.overlay;
let world_bounds = self.settings.sharable.world_bounds;
let clip_rect = painter.clip_rect();
let coding_unit_kind = self.frame.coding_unit_kind(self.settings.sharable.current_plane);
let shapes = self
.frame
.iter_coding_unit_rects(coding_unit_kind)
.filter_map(|world_rect| {
if !self.is_in_bounds(world_rect, clip_rect) {
return None;
}
let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
if is_rect_too_small(screen_rect) {
return None;
}
Some(Shape::rect_stroke(
screen_rect,
Rounding::ZERO,
style.coding_unit_stroke,
))
});
painter.extend(shapes);
None
}
fn draw_motion_vectors(&self, painter: &mut Painter) -> Option<()> {
let _style = &self.settings.persistent.style.overlay;
let world_bounds = self.settings.sharable.world_bounds;
let clip_rect = painter.clip_rect();
let coding_unit_kind = self.frame.coding_unit_kind(self.settings.sharable.current_plane);
// Length of largest motion vector, in screen space.
let mut largest_mv = 1e-9;
let granularity = self.settings.sharable.motion_field.granularity;
let raw_shapes = self
.frame
.iter_coding_units(coding_unit_kind)
.filter_map(|ctx| {
let cu = ctx.coding_unit;
let world_rect = cu.rect();
let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
let Ok(prediction_mode) = cu.get_prediction_mode() else {
return None;
};
let Ok(mode) = self
.frame
.enum_lookup(ProtoEnumMapping::PredictionMode, prediction_mode.mode)
else {
return None;
};
// TODO(comc): Make this a method on PredictionParams or CodingUnit.
let is_motion = !mode.contains("_PRED");
if !is_motion {
return None;
}
let is_compound = mode.contains('_');
let num_mvs = if is_compound { 2 } else { 1 };
// let mv_center = world_rect.center();
let mvs = (0..num_mvs)
.filter_map(|i| {
let mv = prediction_mode.motion_vectors.get(i as usize)?;
let dx = mv.dx as f32 / MOTION_VECTOR_PRECISION;
let dy = mv.dy as f32 / MOTION_VECTOR_PRECISION;
let ref_frame = mv.ref_frame;
if ref_frame == -1 {
return None;
}
let order_hint = mv.ref_frame_order_hint;
// let mv_tip = mv_center + vec2(dx, dy);
let mv_world = vec2(dx, dy);
let mv_screen = mv_world * world_bounds.calc_scale(clip_rect);
// let mv_tip_screen = world_bounds.world_pos_to_screen(mv_tip, clip_rect);
// let screen_pos = screen_rect.center();
// let mv_vector = mv_tip_screen - screen_pos;
let magnitude = mv_screen.length();
if magnitude > largest_mv {
largest_mv = magnitude;
}
let is_future = order_hint > self.frame.frame_params.as_ref().unwrap().raw_display_index;
let color = match self.settings.sharable.motion_field.coloring {
MotionFieldColoring::RefFrames => {
let color_index = ref_frame as usize % 8;
REF_FRAME_COLORS[color_index]
}
MotionFieldColoring::PastFuture => REF_FRAME_COLORS[is_future as usize],
MotionFieldColoring::Monochrome => REF_FRAME_COLORS[0],
};
Some((mv_screen, color))
})
.collect_vec();
if !self.is_in_bounds(world_rect, clip_rect) {
return None;
}
if is_rect_too_small(screen_rect) {
return None;
}
if mvs.is_empty() {
return None;
}
let mv_rects = if self.settings.sharable.motion_field.show {
let mut rects = Vec::new();
let rows = world_rect.height() as i32;
let cols = world_rect.width() as i32;
let top = world_rect.top() as i32;
let left = world_rect.left() as i32;
let granularity_pixels = (granularity * 4) as i32;
let first_row = (granularity_pixels / 2 - top).rem_euclid(granularity_pixels);
let first_col = (granularity_pixels / 2 - left).rem_euclid(granularity_pixels);
for row in (first_row..rows).step_by(granularity_pixels as usize) {
let y = row + top;
for col in (first_col..cols).step_by(granularity_pixels as usize) {
let x = col + left;
let rect = Rect::from_center_size(
pos2(x as f32, y as f32),
vec2(granularity_pixels as f32, granularity_pixels as f32),
);
let screen_rect = world_bounds.world_rect_to_screen(rect, clip_rect);
rects.push(screen_rect);
}
}
rects
} else {
vec![screen_rect]
};
Some((mv_rects, mvs))
})
.collect_vec();
let longest_vector_world_space = granularity as f32 * 2.0;
let longest_vector_screen_space = world_bounds.calc_scale(clip_rect) * longest_vector_world_space;
let normalization_factor = longest_vector_screen_space / largest_mv;
let shapes = raw_shapes.iter().flat_map(|(rects, mvs)| {
let mut mv_shapes = Vec::new();
for screen_rect in rects {
for &(mut mv_vector, color) in mvs.iter() {
if self.settings.sharable.motion_field.normalize && self.settings.sharable.motion_field.show {
mv_vector *= normalization_factor;
}
mv_vector *= self.settings.sharable.motion_field.scale;
let screen_pos = screen_rect.center();
if self.settings.sharable.motion_field.show_origin {
mv_shapes.push(Shape::circle_filled(screen_pos, 1.0, color));
}
mv_shapes.push(Shape::line_segment(
[screen_pos, screen_pos + mv_vector],
Stroke::new(1.5, color),
));
}
}
mv_shapes
});
painter.extend(shapes);
None
}
fn draw_superblocks(&self, painter: &mut Painter) -> Option<()> {
let style = &self.settings.persistent.style.overlay;
let world_bounds = self.settings.sharable.world_bounds;
let clip_rect = painter.clip_rect();
let shapes = self.frame.iter_superblock_rects().filter_map(|world_rect| {
if !self.is_in_bounds(world_rect, clip_rect) {
return None;
}
let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
if is_rect_too_small(screen_rect) {
return None;
}
Some(Shape::rect_stroke(screen_rect, Rounding::ZERO, style.superblock_stroke))
});
painter.extend(shapes);
None
}
fn draw_prediction_modes(&self, painter: &mut Painter) -> Option<()> {
let overlay_style = &self.settings.persistent.style.overlay;
let world_bounds = self.settings.sharable.world_bounds;
let clip_rect = painter.clip_rect();
let coding_unit_kind = self.frame.coding_unit_kind(self.settings.sharable.current_plane);
let ui_style = painter.ctx().style();
let shapes = self
.frame
.iter_coding_units(coding_unit_kind)
.filter_map(|ctx| {
let cu = ctx.coding_unit;
let world_rect = cu.rect();
if !self.is_in_bounds(world_rect, clip_rect) {
return None;
}
let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
if is_rect_too_small(screen_rect) || is_text_too_small(screen_rect) {
return None;
}
let Ok(prediction_mode) = cu.get_prediction_mode() else {
return None;
};
let mode_name = if coding_unit_kind == CodingUnitKind::ChromaOnly {
self.frame
.enum_lookup(ProtoEnumMapping::UvPredictionMode, prediction_mode.uv_mode)
} else {
self.frame
.enum_lookup(ProtoEnumMapping::PredictionMode, prediction_mode.mode)
};
let Ok(mode_name) = mode_name else {
return None;
};
let screen_pos = screen_rect.center();
let mode_name = mode_name.trim_end_matches("_PRED");
let uses_palette = match coding_unit_kind {
CodingUnitKind::Shared | CodingUnitKind::LumaOnly => {
prediction_mode.palette_count > 0
}
CodingUnitKind::ChromaOnly => prediction_mode.uv_palette_count > 0,
};
let uses_intrabc = match coding_unit_kind {
CodingUnitKind::Shared | CodingUnitKind::LumaOnly => {
prediction_mode.use_intrabc
}
CodingUnitKind::ChromaOnly => false
};
let mode_name = if uses_palette {
"PALETTE"
} else if uses_intrabc {
"INTRA_BC"
} else {
mode_name
};
let colors = if overlay_style.enable_text_shadows {
vec![Color32::BLACK, overlay_style.mode_name_color]
} else {
vec![overlay_style.mode_name_color]
};
painter.fonts(|f| {
Some(
colors
.into_iter()
.enumerate()
.map(|(i, color)| {
let pos_offset = vec2(i as f32, i as f32);
Shape::text(
f,
screen_pos + pos_offset,
Align2::CENTER_CENTER,
mode_name,
TextStyle::Body.resolve(&ui_style),
color,
)
})
.collect::<Vec<_>>(),
)
})
})
.flatten()
.collect::<Vec<_>>(); // Note: need to collect here since painter.fonts requires read-only access to painter, and we can't mutate in painter.extend at the same time.
painter.extend(shapes);
None
}
fn draw_transform_modes(&self, painter: &mut Painter) -> Option<()> {
let overlay_style = &self.settings.persistent.style.overlay;
let world_bounds = self.settings.sharable.world_bounds;
let clip_rect = painter.clip_rect();
let ui_style = painter.ctx().style();
let shapes = self
.frame
.iter_transform_units(self.settings.sharable.current_plane.to_plane())
.filter_map(|ctx| {
let tu = ctx.transform_unit;
let world_rect = tu.rect();
if !self.is_in_bounds(world_rect, clip_rect) {
return None;
}
let screen_rect = world_bounds.world_rect_to_screen(world_rect, clip_rect);
if is_rect_too_small(screen_rect) || is_text_too_small(screen_rect) {
return None;
}
let tx_type = tu.primary_tx_type_or_skip(self.frame);
let screen_pos = screen_rect.center();
let colors = if overlay_style.enable_text_shadows {
vec![Color32::BLACK, overlay_style.mode_name_color]
} else {
vec![overlay_style.mode_name_color]
};
painter.fonts(|f| {
Some(
colors
.into_iter()
.enumerate()
.map(|(i, color)| {
let pos_offset = vec2(i as f32, i as f32);
Shape::text(
f,
screen_pos + pos_offset,
Align2::CENTER_CENTER,
tx_type.clone(),
TextStyle::Body.resolve(&ui_style),
color,
)
})
.collect::<Vec<_>>(),
)
})
})
.flatten()
.collect::<Vec<_>>(); // Note: need to collect here since painter.fonts requires read-only access to painter, and we can't mutate in painter.extend at the same time.
painter.extend(shapes);
None
}
}