fix: optimize performance by wrapping trace logs, adding captured drag event subscriptions, and adding regression tests for adjustment compositing.
This commit is contained in:
+40
-14
@@ -384,6 +384,16 @@ pub fn composite_tiled_into(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Applies one adjustment-layer pixel to the current composite row.
|
||||||
|
///
|
||||||
|
/// **Purpose:** Evaluates curves, gradient-map, or hue/saturation data while
|
||||||
|
/// respecting masks, opacity, and blend mode. **Logic & Workflow:** Transparent
|
||||||
|
/// or masked-out destinations exit early; Normal blend bypasses the generic
|
||||||
|
/// blend dispatcher, and fully opaque adjustment pixels are assigned directly.
|
||||||
|
/// **Arguments:** `row` is the destination row, `x`/`gy` are canvas coordinates,
|
||||||
|
/// `layer` owns adjustment and mask data, and `blend`/`opacity` control mixing.
|
||||||
|
/// **Returns:** Nothing. **Side Effects / Dependencies:** Mutates only the RGB
|
||||||
|
/// channels of one destination pixel and uses `hcie-blend` for non-Normal modes.
|
||||||
#[inline]
|
#[inline]
|
||||||
fn blend_adjustment_pixel(
|
fn blend_adjustment_pixel(
|
||||||
row: &mut [u8],
|
row: &mut [u8],
|
||||||
@@ -435,20 +445,36 @@ fn blend_adjustment_pixel(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
let eff_opacity = opacity * (mask_val as f32 / 255.0);
|
||||||
let blended_opaque = blend_pixels(
|
if eff_opacity <= 0.0 {
|
||||||
[dst[0], dst[1], dst[2], 255],
|
return;
|
||||||
[adj_rgb[0], adj_rgb[1], adj_rgb[2], 255],
|
}
|
||||||
blend,
|
|
||||||
1.0,
|
let blended_rgb = if blend == hcie_blend::BlendMode::Normal {
|
||||||
);
|
adj_rgb
|
||||||
row[out_idx] = ((1.0 - eff_opacity) * dst[0] as f32 + eff_opacity * blended_opaque[0] as f32)
|
} else {
|
||||||
.round() as u8;
|
let blended = blend_pixels(
|
||||||
row[out_idx + 1] = ((1.0 - eff_opacity) * dst[1] as f32
|
[dst[0], dst[1], dst[2], 255],
|
||||||
+ eff_opacity * blended_opaque[1] as f32)
|
[adj_rgb[0], adj_rgb[1], adj_rgb[2], 255],
|
||||||
.round() as u8;
|
blend,
|
||||||
row[out_idx + 2] = ((1.0 - eff_opacity) * dst[2] as f32
|
1.0,
|
||||||
+ eff_opacity * blended_opaque[2] as f32)
|
);
|
||||||
.round() as u8;
|
[blended[0], blended[1], blended[2]]
|
||||||
|
};
|
||||||
|
|
||||||
|
if eff_opacity >= 1.0 {
|
||||||
|
row[out_idx] = blended_rgb[0];
|
||||||
|
row[out_idx + 1] = blended_rgb[1];
|
||||||
|
row[out_idx + 2] = blended_rgb[2];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inverse_opacity = 1.0 - eff_opacity;
|
||||||
|
row[out_idx] =
|
||||||
|
(inverse_opacity * dst[0] as f32 + eff_opacity * blended_rgb[0] as f32).round() as u8;
|
||||||
|
row[out_idx + 1] =
|
||||||
|
(inverse_opacity * dst[1] as f32 + eff_opacity * blended_rgb[1] as f32).round() as u8;
|
||||||
|
row[out_idx + 2] =
|
||||||
|
(inverse_opacity * dst[2] as f32 + eff_opacity * blended_rgb[2] as f32).round() as u8;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
//! Regression coverage for optimized Normal-blend adjustment compositing.
|
||||||
|
//!
|
||||||
|
//! **Purpose:** Verifies that bypassing the generic blend dispatcher preserves
|
||||||
|
//! exact RGB output for opaque and partially opaque adjustments.
|
||||||
|
//! **Logic & Workflow:** Builds a one-pixel base and Curves layer, composites
|
||||||
|
//! through the public tiled path, and compares deterministic channel values.
|
||||||
|
//! **Side Effects / Dependencies:** Allocates only one-pixel protocol layers.
|
||||||
|
|
||||||
|
use hcie_blend::Adjustment;
|
||||||
|
use hcie_composite::tiled::composite_tiled;
|
||||||
|
use hcie_protocol::Layer;
|
||||||
|
use hcie_tile::TiledLayer;
|
||||||
|
|
||||||
|
/// Builds a Curves adjustment whose lookup tables map selected input channels.
|
||||||
|
///
|
||||||
|
/// **Arguments:** `opacity` controls the layer-level adjustment mix.
|
||||||
|
/// **Returns:** A one-pixel adjustment layer. **Side Effects / Dependencies:** None.
|
||||||
|
fn curves_layer(opacity: f32) -> Layer {
|
||||||
|
let mut lut_r: Vec<u8> = (0..=255).map(|value| value as u8).collect();
|
||||||
|
let mut lut_g = lut_r.clone();
|
||||||
|
let mut lut_b = lut_r.clone();
|
||||||
|
lut_r[10] = 100;
|
||||||
|
lut_g[20] = 110;
|
||||||
|
lut_b[30] = 120;
|
||||||
|
|
||||||
|
let mut layer = Layer::new_transparent("Curves", 1, 1);
|
||||||
|
layer.adjustment = Some(Adjustment::Curves {
|
||||||
|
lut_r,
|
||||||
|
lut_g,
|
||||||
|
lut_b,
|
||||||
|
});
|
||||||
|
layer.opacity = opacity;
|
||||||
|
layer
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirms a fully opaque Normal adjustment assigns the LUT result exactly.
|
||||||
|
#[test]
|
||||||
|
fn opaque_normal_adjustment_matches_lookup_result() {
|
||||||
|
let base = Layer::from_rgba("Base", 1, 1, vec![10, 20, 30, 255]);
|
||||||
|
let adjustment = curves_layer(1.0);
|
||||||
|
let tiles = vec![
|
||||||
|
Some(TiledLayer::from_dense(&base.pixels, 1, 1)),
|
||||||
|
Some(TiledLayer::from_dense(&adjustment.pixels, 1, 1)),
|
||||||
|
];
|
||||||
|
|
||||||
|
let output = composite_tiled(&[base, adjustment], &tiles, 1, 1);
|
||||||
|
|
||||||
|
assert_eq!(output, vec![100, 110, 120, 255]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Confirms partial opacity retains the previous rounded interpolation behavior.
|
||||||
|
#[test]
|
||||||
|
fn partial_normal_adjustment_preserves_rounded_interpolation() {
|
||||||
|
let base = Layer::from_rgba("Base", 1, 1, vec![10, 20, 30, 255]);
|
||||||
|
let adjustment = curves_layer(0.5);
|
||||||
|
let tiles = vec![
|
||||||
|
Some(TiledLayer::from_dense(&base.pixels, 1, 1)),
|
||||||
|
Some(TiledLayer::from_dense(&adjustment.pixels, 1, 1)),
|
||||||
|
];
|
||||||
|
|
||||||
|
let output = composite_tiled(&[base, adjustment], &tiles, 1, 1);
|
||||||
|
|
||||||
|
assert_eq!(output, vec![55, 65, 75, 255]);
|
||||||
|
}
|
||||||
@@ -145,25 +145,29 @@ impl Engine {
|
|||||||
"[render_composite_region] BEFORE composite: active_idx={}, cache_valid={}, below_cache={}, below_cache_active_idx={:?}, tile_layers_len={}, dirty_rect=[{},{},{},{}]",
|
"[render_composite_region] BEFORE composite: active_idx={}, cache_valid={}, below_cache={}, below_cache_active_idx={:?}, tile_layers_len={}, dirty_rect=[{},{},{},{}]",
|
||||||
active_idx, cache_valid, self.below_cache.is_some(), self.below_cache_active_idx, self.tile_layers.len(), x0, y0, x1, y1
|
active_idx, cache_valid, self.below_cache.is_some(), self.below_cache_active_idx, self.tile_layers.len(), x0, y0, x1, y1
|
||||||
);
|
);
|
||||||
for (i, l) in self.document.layers.iter().enumerate() {
|
if log::log_enabled!(log::Level::Trace) {
|
||||||
let tc = self
|
for (i, l) in self.document.layers.iter().enumerate() {
|
||||||
.tile_layers
|
let tc = self
|
||||||
.get(i)
|
.tile_layers
|
||||||
.and_then(|t| t.as_ref().map(|tl| tl.tile_count()))
|
.get(i)
|
||||||
.unwrap_or(0);
|
.and_then(|t| t.as_ref().map(|tl| tl.tile_count()))
|
||||||
log::trace!(
|
.unwrap_or(0);
|
||||||
"[render_composite_region] layer[{}] id={} visible={} dirty={} opacity={} blend={:?} tile_count={}",
|
log::trace!(
|
||||||
i, l.id, l.visible, l.dirty, l.opacity, l.blend_mode, tc
|
"[render_composite_region] layer[{}] id={} visible={} dirty={} opacity={} blend={:?} tile_count={}",
|
||||||
);
|
i, l.id, l.visible, l.dirty, l.opacity, l.blend_mode, tc
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cache_valid {
|
if cache_valid {
|
||||||
let cache = self.below_cache.as_ref().unwrap();
|
let cache = self.below_cache.as_ref().unwrap();
|
||||||
let below_non_zero = cache.iter().filter(|&&b| b != 0).count();
|
if log::log_enabled!(log::Level::Trace) {
|
||||||
log::trace!(
|
let below_non_zero = cache.iter().filter(|&&b| b != 0).count();
|
||||||
"[render_composite_region] CACHE HIT: using below_cache ({} non-zero bytes), compositing layers[{}..{}] on top",
|
log::trace!(
|
||||||
below_non_zero, active_idx, self.document.layers.len()
|
"[render_composite_region] CACHE HIT: using below_cache ({} non-zero bytes), compositing layers[{}..{}] on top",
|
||||||
);
|
below_non_zero, active_idx, self.document.layers.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
for y in y0..y1 {
|
for y in y0..y1 {
|
||||||
let start = (y as usize * wu + x0u) * 4;
|
let start = (y as usize * wu + x0u) * 4;
|
||||||
let end = start + ((x1 - x0) as usize) * 4;
|
let end = start + ((x1 - x0) as usize) * 4;
|
||||||
|
|||||||
@@ -470,13 +470,15 @@ impl Engine {
|
|||||||
ch,
|
ch,
|
||||||
&mut cache,
|
&mut cache,
|
||||||
);
|
);
|
||||||
let non_zero = cache.iter().filter(|&&b| b != 0).count();
|
if log::log_enabled!(log::Level::Trace) {
|
||||||
log::trace!(
|
let non_zero = cache.iter().filter(|&&b| b != 0).count();
|
||||||
"[begin_stroke] below_cache built: {} bytes, non-zero bytes={}, active_idx={}",
|
log::trace!(
|
||||||
cache.len(),
|
"[begin_stroke] below_cache built: {} bytes, non-zero bytes={}, active_idx={}",
|
||||||
non_zero,
|
cache.len(),
|
||||||
active_idx
|
non_zero,
|
||||||
);
|
active_idx
|
||||||
|
);
|
||||||
|
}
|
||||||
self.below_cache = Some(cache);
|
self.below_cache = Some(cache);
|
||||||
self.below_cache_active_idx = Some(active_idx);
|
self.below_cache_active_idx = Some(active_idx);
|
||||||
self.below_cache_dirty = false;
|
self.below_cache_dirty = false;
|
||||||
|
|||||||
@@ -3550,6 +3550,7 @@ impl HcieIcedApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Message::CanvasCursorPos { x, y } => {
|
Message::CanvasCursorPos { x, y } => {
|
||||||
|
crate::canvas::perf::record_idle_pointer_message(false);
|
||||||
self.canvas_cursor_pos = Some((x, y));
|
self.canvas_cursor_pos = Some((x, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4707,6 +4708,7 @@ impl HcieIcedApp {
|
|||||||
self.layer_style_dragging = true;
|
self.layer_style_dragging = true;
|
||||||
}
|
}
|
||||||
Message::LayerStyleDialogDragMove(x, y) | Message::DockPointerMoved(x, y) => {
|
Message::LayerStyleDialogDragMove(x, y) | Message::DockPointerMoved(x, y) => {
|
||||||
|
crate::canvas::perf::record_idle_pointer_message(true);
|
||||||
self.last_cursor_pos = Some((x, y));
|
self.last_cursor_pos = Some((x, y));
|
||||||
let pointer = iced::Point::new(x, y);
|
let pointer = iced::Point::new(x, y);
|
||||||
if self.dock.drag.is_none() {
|
if self.dock.drag.is_none() {
|
||||||
@@ -9677,13 +9679,19 @@ impl HcieIcedApp {
|
|||||||
colors,
|
colors,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let _elapsed = view_start.elapsed();
|
let elapsed = view_start.elapsed();
|
||||||
|
if !self.tool_state.is_drawing {
|
||||||
|
crate::canvas::perf::record_idle_view(elapsed);
|
||||||
|
}
|
||||||
// Performance log measuring Elm view reconstruction time.
|
// Performance log measuring Elm view reconstruction time.
|
||||||
// useful to track widget layout allocation and view model reconstruction times.
|
// useful to track widget layout allocation and view model reconstruction times.
|
||||||
// log::info!("[perf] view(): {:.1}ms", elapsed.as_secs_f64() * 1000.0);
|
// log::info!("[perf] view(): {:.1}ms", elapsed.as_secs_f64() * 1000.0);
|
||||||
stack.width(Length::Fill).height(Length::Fill).into()
|
stack.width(Length::Fill).height(Length::Fill).into()
|
||||||
} else {
|
} else {
|
||||||
let _elapsed = view_start.elapsed();
|
let elapsed = view_start.elapsed();
|
||||||
|
if !self.tool_state.is_drawing {
|
||||||
|
crate::canvas::perf::record_idle_view(elapsed);
|
||||||
|
}
|
||||||
// Performance log measuring Elm view reconstruction time.
|
// Performance log measuring Elm view reconstruction time.
|
||||||
// log::info!("[perf] view(): {:.1}ms", elapsed.as_secs_f64() * 1000.0);
|
// log::info!("[perf] view(): {:.1}ms", elapsed.as_secs_f64() * 1000.0);
|
||||||
content.into()
|
content.into()
|
||||||
@@ -9933,7 +9941,9 @@ impl HcieIcedApp {
|
|||||||
iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left) => (status
|
iced::mouse::Event::ButtonPressed(iced::mouse::Button::Left) => (status
|
||||||
== iced::event::Status::Ignored)
|
== iced::event::Status::Ignored)
|
||||||
.then_some(Message::OverlayOutsideClick),
|
.then_some(Message::OverlayOutsideClick),
|
||||||
iced::mouse::Event::CursorMoved { position } => {
|
iced::mouse::Event::CursorMoved { position }
|
||||||
|
if status == iced::event::Status::Ignored =>
|
||||||
|
{
|
||||||
Some(Message::DockPointerMoved(position.x, position.y))
|
Some(Message::DockPointerMoved(position.x, position.y))
|
||||||
}
|
}
|
||||||
iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left) => {
|
iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left) => {
|
||||||
@@ -9982,6 +9992,27 @@ impl HcieIcedApp {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Captured canvas events normally stay local to the retained overlay.
|
||||||
|
// During a real global drag, forward captured movement so dock/dialog
|
||||||
|
// interactions continue across the canvas without rebuilding the app
|
||||||
|
// for every idle canvas movement.
|
||||||
|
let captured_drag_pointer = if self.layer_style_dragging
|
||||||
|
|| self.dock.header_drag.is_some()
|
||||||
|
|| self.dock.drag.is_some()
|
||||||
|
|| self.dock.floating_drag.is_some()
|
||||||
|
{
|
||||||
|
iced::event::listen_with(|event, status, _id| match event {
|
||||||
|
iced::Event::Mouse(iced::mouse::Event::CursorMoved { position })
|
||||||
|
if status == iced::event::Status::Captured =>
|
||||||
|
{
|
||||||
|
Some(Message::DockPointerMoved(position.x, position.y))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
iced::Subscription::none()
|
||||||
|
};
|
||||||
|
|
||||||
// The marching ants are rendered in the persistent GPU canvas shader.
|
// The marching ants are rendered in the persistent GPU canvas shader.
|
||||||
// Use a real timer rather than an immediately-ready async loop: the old
|
// Use a real timer rather than an immediately-ready async loop: the old
|
||||||
// loop generated an unbounded stream of CompositeRefresh messages and
|
// loop generated an unbounded stream of CompositeRefresh messages and
|
||||||
@@ -10010,7 +10041,13 @@ impl HcieIcedApp {
|
|||||||
iced::Subscription::none()
|
iced::Subscription::none()
|
||||||
};
|
};
|
||||||
|
|
||||||
iced::Subscription::batch(vec![keyboard, mouse, marching_ants, stroke_frames])
|
iced::Subscription::batch(vec![
|
||||||
|
keyboard,
|
||||||
|
mouse,
|
||||||
|
captured_drag_pointer,
|
||||||
|
marching_ants,
|
||||||
|
stroke_frames,
|
||||||
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ struct OverlayProgram {
|
|||||||
_quick_mask: bool,
|
_quick_mask: bool,
|
||||||
/// Active brush diameter in canvas pixels for the footprint cursor.
|
/// Active brush diameter in canvas pixels for the footprint cursor.
|
||||||
brush_size: f32,
|
brush_size: f32,
|
||||||
|
/// Whether a raster stroke is active; used to keep idle latency metrics isolated.
|
||||||
|
is_drawing: bool,
|
||||||
/// Normalized bounds of the selected vector shape in canvas-space (x1, y1, x2, y2).
|
/// Normalized bounds of the selected vector shape in canvas-space (x1, y1, x2, y2).
|
||||||
selected_vector_bounds: Option<(f32, f32, f32, f32)>,
|
selected_vector_bounds: Option<(f32, f32, f32, f32)>,
|
||||||
/// Rotation angle (radians) of the selected vector shape.
|
/// Rotation angle (radians) of the selected vector shape.
|
||||||
@@ -108,6 +110,10 @@ struct OverlayProgram {
|
|||||||
struct OverlayState {
|
struct OverlayState {
|
||||||
/// Current cursor position in viewport-local coordinates.
|
/// Current cursor position in viewport-local coordinates.
|
||||||
cursor_pos: Option<Point>,
|
cursor_pos: Option<Point>,
|
||||||
|
/// Monotonic timestamp of the latest cursor event for opt-in latency diagnostics.
|
||||||
|
cursor_event_at: Option<std::time::Instant>,
|
||||||
|
/// Sequence used to avoid measuring duplicate redraws of one cursor event.
|
||||||
|
cursor_event_sequence: u64,
|
||||||
/// Whether the mouse is over the overlay.
|
/// Whether the mouse is over the overlay.
|
||||||
is_hovered: bool,
|
is_hovered: bool,
|
||||||
/// Current handle being hovered (for cursor changes).
|
/// Current handle being hovered (for cursor changes).
|
||||||
@@ -2050,6 +2056,14 @@ impl canvas::Program<Message> for OverlayProgram {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
geometries.push(crosshair_frame.into_geometry());
|
geometries.push(crosshair_frame.into_geometry());
|
||||||
|
if !self.is_drawing {
|
||||||
|
if let Some(captured_at) = state.cursor_event_at {
|
||||||
|
crate::canvas::perf::record_idle_cursor_draw(
|
||||||
|
state.cursor_event_sequence,
|
||||||
|
captured_at,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2096,6 +2110,8 @@ impl canvas::Program<Message> for OverlayProgram {
|
|||||||
canvas::Event::Mouse(mouse::Event::CursorMoved { position }) => {
|
canvas::Event::Mouse(mouse::Event::CursorMoved { position }) => {
|
||||||
let local_pos = Point::new(position.x - bounds.x, position.y - bounds.y);
|
let local_pos = Point::new(position.x - bounds.x, position.y - bounds.y);
|
||||||
state.cursor_pos = Some(local_pos);
|
state.cursor_pos = Some(local_pos);
|
||||||
|
state.cursor_event_at = Some(std::time::Instant::now());
|
||||||
|
state.cursor_event_sequence = state.cursor_event_sequence.wrapping_add(1);
|
||||||
state.is_hovered = bounds.contains(position);
|
state.is_hovered = bounds.contains(position);
|
||||||
|
|
||||||
// Update hovered handle
|
// Update hovered handle
|
||||||
@@ -2305,6 +2321,7 @@ pub fn view<'a>(
|
|||||||
polygon_points: doc.polygon_points.clone(),
|
polygon_points: doc.polygon_points.clone(),
|
||||||
_quick_mask: doc.quick_mask,
|
_quick_mask: doc.quick_mask,
|
||||||
brush_size: tool_state.brush_size,
|
brush_size: tool_state.brush_size,
|
||||||
|
is_drawing: tool_state.is_drawing,
|
||||||
selected_vector_bounds: sel_vec_bounds,
|
selected_vector_bounds: sel_vec_bounds,
|
||||||
selected_vector_angle: sel_vec_angle,
|
selected_vector_angle: sel_vec_angle,
|
||||||
vector_edit_handle: doc.vector_edit_handle,
|
vector_edit_handle: doc.vector_edit_handle,
|
||||||
|
|||||||
@@ -27,6 +27,92 @@ struct PerfState {
|
|||||||
latest_render_input: Option<Instant>,
|
latest_render_input: Option<Instant>,
|
||||||
last_prepare: Option<Instant>,
|
last_prepare: Option<Instant>,
|
||||||
finish_pending: bool,
|
finish_pending: bool,
|
||||||
|
idle_cursor_samples_ms: Vec<f64>,
|
||||||
|
idle_view_samples_ms: Vec<f64>,
|
||||||
|
idle_canvas_messages: u64,
|
||||||
|
idle_dock_messages: u64,
|
||||||
|
last_idle_cursor_sequence: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records one application view reconstruction during cursor diagnostics.
|
||||||
|
///
|
||||||
|
/// **Arguments:** `elapsed` is the time spent constructing the Elm element tree.
|
||||||
|
/// **Returns:** Nothing. **Side Effects / Dependencies:** Stores a bounded sample
|
||||||
|
/// only when `HCIE_CANVAS_PERF=1`; reporting is performed by cursor draw samples.
|
||||||
|
pub fn record_idle_view(elapsed: Duration) {
|
||||||
|
if !enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
with_state(|state| {
|
||||||
|
if state.idle_view_samples_ms.len() < 240 {
|
||||||
|
state
|
||||||
|
.idle_view_samples_ms
|
||||||
|
.push(elapsed.as_secs_f64() * 1000.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Counts an app-level message generated by idle pointer movement.
|
||||||
|
///
|
||||||
|
/// **Arguments:** `dock_message` is `true` for the global dock subscription and
|
||||||
|
/// `false` for canvas status coordinates. **Returns:** Nothing.
|
||||||
|
/// **Side Effects / Dependencies:** Updates diagnostic counters only when enabled.
|
||||||
|
pub fn record_idle_pointer_message(dock_message: bool) {
|
||||||
|
if !enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
with_state(|state| {
|
||||||
|
if dock_message {
|
||||||
|
state.idle_dock_messages = state.idle_dock_messages.saturating_add(1);
|
||||||
|
} else {
|
||||||
|
state.idle_canvas_messages = state.idle_canvas_messages.saturating_add(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Records native cursor-event to overlay-draw latency and periodically reports it.
|
||||||
|
///
|
||||||
|
/// **Arguments:** `sequence` identifies a unique retained-overlay event and
|
||||||
|
/// `captured_at` is its monotonic timestamp. **Returns:** Nothing.
|
||||||
|
/// **Side Effects / Dependencies:** Emits one summary per 120 unique cursor draws
|
||||||
|
/// when `HCIE_CANVAS_PERF=1`; duplicate draws of the same event are ignored.
|
||||||
|
pub fn record_idle_cursor_draw(sequence: u64, captured_at: Instant) {
|
||||||
|
if !enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
with_state(|state| {
|
||||||
|
if state.last_idle_cursor_sequence == Some(sequence) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.last_idle_cursor_sequence = Some(sequence);
|
||||||
|
state
|
||||||
|
.idle_cursor_samples_ms
|
||||||
|
.push(captured_at.elapsed().as_secs_f64() * 1000.0);
|
||||||
|
if state.idle_cursor_samples_ms.len() < 120 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cursor_samples = std::mem::take(&mut state.idle_cursor_samples_ms);
|
||||||
|
let mut view_samples = std::mem::take(&mut state.idle_view_samples_ms);
|
||||||
|
cursor_samples.sort_by(f64::total_cmp);
|
||||||
|
view_samples.sort_by(f64::total_cmp);
|
||||||
|
log::info!(
|
||||||
|
target: "hcie_canvas_perf",
|
||||||
|
"idle_cursor draws={} event_to_overlay_draw[p50={:.3}ms,p95={:.3}ms,max={:.3}ms] view_rebuild[n={},p50={:.3}ms,p95={:.3}ms,max={:.3}ms] messages[canvas={},dock={}]",
|
||||||
|
cursor_samples.len(),
|
||||||
|
percentile(&cursor_samples, 0.50),
|
||||||
|
percentile(&cursor_samples, 0.95),
|
||||||
|
cursor_samples.last().copied().unwrap_or(0.0),
|
||||||
|
view_samples.len(),
|
||||||
|
percentile(&view_samples, 0.50),
|
||||||
|
percentile(&view_samples, 0.95),
|
||||||
|
view_samples.last().copied().unwrap_or(0.0),
|
||||||
|
state.idle_canvas_messages,
|
||||||
|
state.idle_dock_messages,
|
||||||
|
);
|
||||||
|
state.idle_canvas_messages = 0;
|
||||||
|
state.idle_dock_messages = 0;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether diagnostics are enabled for this process.
|
/// Returns whether diagnostics are enabled for this process.
|
||||||
|
|||||||
@@ -963,6 +963,30 @@ pub struct CanvasShaderState {
|
|||||||
pub space_left_pan: bool,
|
pub space_left_pan: bool,
|
||||||
/// Immediate local pan offset maintained during active panning to eliminate 1-frame Elm update latency.
|
/// Immediate local pan offset maintained during active panning to eliminate 1-frame Elm update latency.
|
||||||
pub local_pan_offset: Option<Vector>,
|
pub local_pan_offset: Option<Vector>,
|
||||||
|
/// Last canvas coordinate published to the status bar.
|
||||||
|
pub last_status_cursor: Option<(u32, u32)>,
|
||||||
|
/// Time of the last status-bar coordinate publication.
|
||||||
|
pub last_status_emit: Option<std::time::Instant>,
|
||||||
|
/// Last pane size sent to application state, stored as exact float bit patterns.
|
||||||
|
pub last_reported_pane_size: Option<(u32, u32)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Minimum interval between status-bar cursor messages during idle movement.
|
||||||
|
const CURSOR_STATUS_INTERVAL: std::time::Duration = std::time::Duration::from_millis(500);
|
||||||
|
|
||||||
|
/// Decides whether an idle cursor coordinate needs an Elm application message.
|
||||||
|
///
|
||||||
|
/// **Arguments:** `previous` and `current` are integer canvas coordinates;
|
||||||
|
/// `elapsed` is time since the previous publication, or `None` for the first.
|
||||||
|
/// **Returns:** `true` only for a changed coordinate whose interval is due.
|
||||||
|
/// **Side Effects / Dependencies:** None.
|
||||||
|
fn should_publish_cursor_status(
|
||||||
|
previous: Option<(u32, u32)>,
|
||||||
|
current: (u32, u32),
|
||||||
|
elapsed: Option<std::time::Duration>,
|
||||||
|
) -> bool {
|
||||||
|
previous != Some(current)
|
||||||
|
&& elapsed.map_or(true, |duration| duration >= CURSOR_STATUS_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Canvas shader program — implements `iced::widget::shader::Program`.
|
/// Canvas shader program — implements `iced::widget::shader::Program`.
|
||||||
@@ -1070,8 +1094,6 @@ impl shader::Program<Message> for CanvasShaderProgram {
|
|||||||
state.cursor_pos = Some(local_pos);
|
state.cursor_pos = Some(local_pos);
|
||||||
state.is_hovered = bounds.contains(position);
|
state.is_hovered = bounds.contains(position);
|
||||||
|
|
||||||
let pane_size_msg = Message::CanvasSize((bounds.width, bounds.height));
|
|
||||||
|
|
||||||
let (canvas_x, canvas_y) = screen_to_canvas_local(
|
let (canvas_x, canvas_y) = screen_to_canvas_local(
|
||||||
local_pos,
|
local_pos,
|
||||||
bounds.width,
|
bounds.width,
|
||||||
@@ -1139,16 +1161,38 @@ impl shader::Program<Message> for CanvasShaderProgram {
|
|||||||
|
|
||||||
// Cursor over canvas → status bar coords
|
// Cursor over canvas → status bar coords
|
||||||
if let (Some(cx), Some(cy)) = (canvas_x, canvas_y) {
|
if let (Some(cx), Some(cy)) = (canvas_x, canvas_y) {
|
||||||
return (
|
let current = (cx as u32, cy as u32);
|
||||||
iced::event::Status::Captured,
|
let now = std::time::Instant::now();
|
||||||
Some(Message::CanvasCursorPos {
|
let elapsed = state
|
||||||
x: cx as u32,
|
.last_status_emit
|
||||||
y: cy as u32,
|
.map(|previous| now.duration_since(previous));
|
||||||
}),
|
if should_publish_cursor_status(
|
||||||
);
|
state.last_status_cursor,
|
||||||
|
current,
|
||||||
|
elapsed,
|
||||||
|
) {
|
||||||
|
state.last_status_cursor = Some(current);
|
||||||
|
state.last_status_emit = Some(now);
|
||||||
|
return (
|
||||||
|
iced::event::Status::Captured,
|
||||||
|
Some(Message::CanvasCursorPos {
|
||||||
|
x: current.0,
|
||||||
|
y: current.1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (iced::event::Status::Captured, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (iced::event::Status::Captured, Some(pane_size_msg));
|
let pane_size = (bounds.width.to_bits(), bounds.height.to_bits());
|
||||||
|
if state.last_reported_pane_size != Some(pane_size) {
|
||||||
|
state.last_reported_pane_size = Some(pane_size);
|
||||||
|
return (
|
||||||
|
iced::event::Status::Captured,
|
||||||
|
Some(Message::CanvasSize((bounds.width, bounds.height))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (iced::event::Status::Captured, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
mouse::Event::ButtonPressed(button) => {
|
mouse::Event::ButtonPressed(button) => {
|
||||||
@@ -1365,10 +1409,38 @@ impl shader::Program<Message> for CanvasShaderProgram {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod shader_regression_tests {
|
mod shader_regression_tests {
|
||||||
|
use super::{should_publish_cursor_status, CURSOR_STATUS_INTERVAL};
|
||||||
|
|
||||||
/// Prevents restoration of the nine-sample per-fragment selection border detector.
|
/// Prevents restoration of the nine-sample per-fragment selection border detector.
|
||||||
#[test]
|
#[test]
|
||||||
fn selection_overlay_uses_one_texture_sample() {
|
fn selection_overlay_uses_one_texture_sample() {
|
||||||
let shader = include_str!("canvas.wgsl");
|
let shader = include_str!("canvas.wgsl");
|
||||||
assert_eq!(shader.matches("textureSample(selection_texture").count(), 1);
|
assert_eq!(shader.matches("textureSample(selection_texture").count(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Confirms status-bar messages are bounded without suppressing the first update.
|
||||||
|
///
|
||||||
|
/// **Purpose:** Protects the retained overlay from accidental per-pointer Elm
|
||||||
|
/// rebuilds. **Logic & Workflow:** Exercises first, early, due, and unchanged
|
||||||
|
/// cursor publications. **Arguments & Returns:** None.
|
||||||
|
/// **Side Effects / Dependencies:** None.
|
||||||
|
#[test]
|
||||||
|
fn idle_cursor_status_updates_are_rate_limited() {
|
||||||
|
assert!(should_publish_cursor_status(None, (10, 20), None));
|
||||||
|
assert!(!should_publish_cursor_status(
|
||||||
|
Some((10, 20)),
|
||||||
|
(11, 20),
|
||||||
|
Some(CURSOR_STATUS_INTERVAL / 2),
|
||||||
|
));
|
||||||
|
assert!(should_publish_cursor_status(
|
||||||
|
Some((10, 20)),
|
||||||
|
(11, 20),
|
||||||
|
Some(CURSOR_STATUS_INTERVAL),
|
||||||
|
));
|
||||||
|
assert!(!should_publish_cursor_status(
|
||||||
|
Some((11, 20)),
|
||||||
|
(11, 20),
|
||||||
|
Some(CURSOR_STATUS_INTERVAL * 2),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
Build:1022
|
Build:1022
|
||||||
|
Egui ile Iced arasında hız farkı var. Egui hızlı . Ama o kadar büyük değil. Zaman zaman bu gerileme ortaya çıkıyor. O yüzden buraya karşılaştırma için dosyaları bırakıyorum.
|
||||||
|
|
||||||
|
commit : f2cca7e6a19daa9f470680260bfdf24746f214ee
|
||||||
|
DÜZELTME
|
||||||
|
feat: implement file opening logic in a new tab and streamline document loading process
|
||||||
|
|
||||||
commit : a2264b130d47fbbd565226040bc28fd5b9626177
|
commit : a2264b130d47fbbd565226040bc28fd5b9626177
|
||||||
4K MULTI LAYEPERFORMANCE OK : SOL 5.6 HIGH feat(perf): add real-time canvas performance measurements
|
4K MULTI LAYEPERFORMANCE OK : SOL 5.6 HIGH feat(perf): add real-time canvas performance measurements
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user