Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd8e6c1bda | ||
|
|
2ca84676ee | ||
|
|
336720559b | ||
|
|
636093aae6 | ||
|
|
c0908f101a | ||
|
|
24b1eab07d | ||
|
|
47f55199b4 | ||
|
|
41a23fbbe7 | ||
|
|
1a372f1b68 | ||
|
|
bb6e8a44ac | ||
|
|
1a669130fb | ||
|
|
715036e132 | ||
|
|
a9cd364eac |
@@ -210,7 +210,6 @@ impl SemanticPlan {
|
||||
pub fn project(&self) -> &Project {
|
||||
&self.candidate
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub fn index(&self) -> &ViewIndex {
|
||||
&self.index
|
||||
}
|
||||
@@ -485,6 +484,57 @@ pub struct PresentationContext {
|
||||
pub target: Option<TargetPresentation>,
|
||||
}
|
||||
|
||||
impl PresentationContext {
|
||||
fn header_visible(self, header: Rect, outgoing: bool) -> bool {
|
||||
if !outgoing {
|
||||
return self.viewport.contains(header);
|
||||
}
|
||||
// Leaving a performer must not require fitting the entire work card
|
||||
// inside the camera. Retain a 24px grab region so the result stays
|
||||
// directly reachable; geometry/model admission is independent.
|
||||
let width = (header.position.x + header.size.width)
|
||||
.min(self.viewport.position.x + self.viewport.size.width)
|
||||
- header.position.x.max(self.viewport.position.x);
|
||||
let height = (header.position.y + header.size.height)
|
||||
.min(self.viewport.position.y + self.viewport.size.height)
|
||||
- header.position.y.max(self.viewport.position.y);
|
||||
width * self.zoom >= 24. && height * self.zoom >= 24.
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outgoing_header_retains_a_grab_region_without_requiring_full_camera_containment() {
|
||||
let context = PresentationContext {
|
||||
viewport: Rect {
|
||||
position: Position { x: 0., y: 0. },
|
||||
size: Size {
|
||||
width: 800.,
|
||||
height: 400.,
|
||||
},
|
||||
},
|
||||
zoom: 0.75,
|
||||
target: None,
|
||||
};
|
||||
let mut header = Rect {
|
||||
position: Position { x: -100., y: 30. },
|
||||
size: Size {
|
||||
width: 224.,
|
||||
height: 62.,
|
||||
},
|
||||
};
|
||||
assert!(context.header_visible(header, true));
|
||||
assert!(!context.header_visible(header, false));
|
||||
header.position.x = -192.; // exactly 32 world pixels = 24 logical pixels
|
||||
assert!(context.header_visible(header, true));
|
||||
header.position.x -= 1.;
|
||||
assert!(!context.header_visible(header, true));
|
||||
header.position.x = 0.;
|
||||
header.position.y = 368.;
|
||||
assert!(context.header_visible(header, true));
|
||||
header.position.y += 1.;
|
||||
assert!(!context.header_visible(header, true));
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PlacementPlan {
|
||||
pub diagram: Diagram,
|
||||
@@ -496,8 +546,8 @@ pub struct PlacementPlan {
|
||||
commands: Vec<Command>,
|
||||
}
|
||||
|
||||
/// `requested` is the original selected placement position plus pointer motion
|
||||
/// in the frozen transform (therefore preserving the press offset). Snap only
|
||||
/// `requested` preserves the frozen pointer offset unless the caller proposes
|
||||
/// bounded outward clearance for an outgoing drop. Snap only
|
||||
/// this root; apply the resulting identical delta to captured Q and its routes.
|
||||
/// No whole Project clone, validator or semantic ViewIndex rebuild occurs here.
|
||||
pub fn place(
|
||||
@@ -577,7 +627,7 @@ pub fn place(
|
||||
height: selected.size.height.min(62.),
|
||||
},
|
||||
};
|
||||
if !presentation.viewport.contains(header) {
|
||||
if !presentation.header_visible(header, plan.destination == Destination::OutsidePerformers) {
|
||||
return Err(Refusal::LayoutRequired(
|
||||
"Make room or adjust the view, then drag again.".into(),
|
||||
));
|
||||
|
||||
@@ -188,7 +188,7 @@ pub fn place(
|
||||
};
|
||||
geometry::check_rigid(&plan.before_expanded, &expanded, &plan.placements, delta)?;
|
||||
scenes?;
|
||||
geometry::check_headers(&view, &plan.selected, presentation.viewport)?;
|
||||
geometry::check_headers(&view, &plan.selected, presentation, false)?;
|
||||
Ok(PlacementPlan {
|
||||
diagram,
|
||||
view,
|
||||
|
||||
@@ -398,7 +398,12 @@ pub fn place(
|
||||
&expanded,
|
||||
)?;
|
||||
geometry::check_rigid(&scope.0.before_expanded, &expanded, scope.affected(), delta)?;
|
||||
geometry::check_headers(&view, scope.selected(), presentation.viewport)?;
|
||||
geometry::check_headers(
|
||||
&view,
|
||||
scope.selected(),
|
||||
presentation,
|
||||
plan.destination == Destination::OutsidePerformers,
|
||||
)?;
|
||||
Ok(PlacementPlan {
|
||||
diagram,
|
||||
view,
|
||||
|
||||
@@ -170,7 +170,8 @@ pub(super) fn check_rigid(
|
||||
pub(super) fn check_headers(
|
||||
view: &ProjectedView,
|
||||
selected: &BTreeMap<Id, Id>,
|
||||
viewport: Rect,
|
||||
presentation: PresentationContext,
|
||||
outgoing: bool,
|
||||
) -> Result<(), Refusal> {
|
||||
for (id, element) in selected {
|
||||
let node = view
|
||||
@@ -185,7 +186,7 @@ pub(super) fn check_headers(
|
||||
height: node.size.height.min(62.),
|
||||
},
|
||||
};
|
||||
if !viewport.contains(header) {
|
||||
if !presentation.header_visible(header, outgoing) {
|
||||
return Err(Refusal::LayoutRequired(
|
||||
"Make room or adjust the view, then drag again.".into(),
|
||||
));
|
||||
|
||||
@@ -14,6 +14,8 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
mod clearance;
|
||||
|
||||
pub const FINISH_EDIT: &str =
|
||||
"Finish or cancel the current edit before selecting or moving elements.";
|
||||
|
||||
@@ -56,6 +58,7 @@ pub struct Target {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Frozen {
|
||||
pub targets: Vec<Target>,
|
||||
pub moving: Vec<Rectangle>,
|
||||
cards: Vec<Rectangle>,
|
||||
controls: Vec<Rectangle>,
|
||||
edges: Vec<(Point, Point)>,
|
||||
@@ -82,14 +85,16 @@ impl Frozen {
|
||||
) -> Self {
|
||||
let mut targets = Vec::new();
|
||||
let mut cards = Vec::new();
|
||||
let mut moving = Vec::new();
|
||||
for n in &view.nodes {
|
||||
if affected.contains_key(&n.id) {
|
||||
continue;
|
||||
}
|
||||
let region = Rectangle::new(
|
||||
Point::new(n.position.x, n.position.y),
|
||||
iced::Size::new(n.size.width, n.size.height),
|
||||
);
|
||||
if affected.contains_key(&n.id) {
|
||||
moving.push(region);
|
||||
continue;
|
||||
}
|
||||
let Some(e) = project.elements.get(&n.element) else {
|
||||
cards.push(region);
|
||||
continue;
|
||||
@@ -172,6 +177,7 @@ impl Frozen {
|
||||
}
|
||||
Self {
|
||||
targets,
|
||||
moving,
|
||||
cards,
|
||||
controls,
|
||||
edges,
|
||||
@@ -407,7 +413,6 @@ impl Candidate {
|
||||
Self::Performers(p) => &p.accepted,
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub fn index(&self) -> &view_options::ViewIndex {
|
||||
match self {
|
||||
Self::Singleton(p) => p.index(),
|
||||
@@ -525,6 +530,8 @@ pub(crate) struct Feedback {
|
||||
pub inspection_targets: Vec<usize>,
|
||||
pub text: String,
|
||||
pub refused: bool,
|
||||
/// Pointer-following outline only; it never authorizes a model change.
|
||||
pub ghost_delta: Option<Vector>,
|
||||
}
|
||||
impl Feedback {
|
||||
pub fn inspection_identity(&self) -> String {
|
||||
@@ -1055,7 +1062,26 @@ impl App {
|
||||
viewport: stamp.viewport(), zoom: stamp.zoom,
|
||||
target: target.map(|i| plan::TargetPresentation { placement: active.frozen.as_ref().unwrap().targets[i].placement }),
|
||||
};
|
||||
let placed = candidate.place(requested, presentation).map_err(|r| refusal(&r))?;
|
||||
let exact = candidate.place(requested, presentation);
|
||||
let placed = if destination == plan::Destination::OutsidePerformers
|
||||
&& matches!(&exact, Err(plan::Refusal::LayoutRequired(_)))
|
||||
{
|
||||
let affected = match &active.prepared {
|
||||
Prepared::Singleton(scope) => Some(&scope.placements),
|
||||
Prepared::Assignment(scope) => Some(scope.affected()),
|
||||
_ => None,
|
||||
};
|
||||
affected.and_then(|affected| {
|
||||
clearance::requests(
|
||||
(self.project(), self.document.view_index()),
|
||||
(candidate.project(), candidate.index()), stamp.context.diagram,
|
||||
affected, *anchor, requested,
|
||||
(stamp.world(press.start), stamp.world(pointer.unwrap())),
|
||||
).into_iter().find_map(|position| candidate.place(position, presentation).ok())
|
||||
}).map_or(exact, Ok)
|
||||
} else {
|
||||
exact
|
||||
}.map_err(|r| refusal(&r))?;
|
||||
let label = candidate.label();
|
||||
if release {
|
||||
if let Some(command) = candidate.finish(self.project(), &placed).map_err(|r| refusal(&r))? {
|
||||
@@ -1082,6 +1108,14 @@ impl App {
|
||||
} else {
|
||||
let inspection_targets = active.inspection_targets.clone();
|
||||
let frozen = active.frozen.clone();
|
||||
// A rejected intermediate position is not a return gesture. Keep the
|
||||
// last admitted scene and show current motion separately; release
|
||||
// always recomputes admission and never commits this retained view.
|
||||
if refused && in_bounds && preview.is_none() {
|
||||
preview = self.allocation_drag.preview.take();
|
||||
}
|
||||
let ghost_delta = (refused && in_bounds)
|
||||
.then(|| (pointer.unwrap() - press.start) * (1. / stamp.zoom));
|
||||
self.allocation_drag.active = Some(active);
|
||||
self.allocation_drag.preview = preview;
|
||||
self.allocation_drag.feedback = Some(Feedback {
|
||||
@@ -1092,6 +1126,7 @@ impl App {
|
||||
inspection_targets,
|
||||
text,
|
||||
refused,
|
||||
ghost_delta,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,595 @@
|
||||
//! Bounded outgoing clearance proposals. The ordinary placement and semantic
|
||||
//! validators still decide whether any proposal may be previewed or committed.
|
||||
use iced::Point;
|
||||
use oa_core::{Id, NodePlacement, Position, Project, projection, view_options::ViewIndex};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
const GRID: f32 = 10.;
|
||||
const GAP: f32 = 10.;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Bounds {
|
||||
left: f32,
|
||||
top: f32,
|
||||
right: f32,
|
||||
bottom: f32,
|
||||
}
|
||||
|
||||
impl Bounds {
|
||||
fn node(node: &projection::ProjectedNode) -> Option<Self> {
|
||||
let result = Self {
|
||||
left: node.position.x,
|
||||
top: node.position.y,
|
||||
right: node.position.x + node.size.width,
|
||||
bottom: node.position.y + node.size.height,
|
||||
};
|
||||
([result.left, result.top, result.right, result.bottom]
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
&& result.right > result.left
|
||||
&& result.bottom > result.top)
|
||||
.then_some(result)
|
||||
}
|
||||
|
||||
fn union(self, other: Self) -> Self {
|
||||
Self {
|
||||
left: self.left.min(other.left),
|
||||
top: self.top.min(other.top),
|
||||
right: self.right.max(other.right),
|
||||
bottom: self.bottom.max(other.bottom),
|
||||
}
|
||||
}
|
||||
|
||||
fn overlaps(self, other: Self) -> bool {
|
||||
self.left < other.right
|
||||
&& other.left < self.right
|
||||
&& self.top < other.bottom
|
||||
&& other.top < self.bottom
|
||||
}
|
||||
}
|
||||
|
||||
/// Called only after an OutsidePerformers candidate fails ordinary placement.
|
||||
/// Coordinates use the frozen world transform. The entire affected footprint
|
||||
/// receives one anchor translation; this never moves or packs other cards.
|
||||
pub(super) fn requests(
|
||||
accepted: (&Project, &ViewIndex),
|
||||
candidate: (&Project, &ViewIndex),
|
||||
diagram: Id,
|
||||
affected: &BTreeMap<Id, NodePlacement>,
|
||||
anchor: Id,
|
||||
requested: Position,
|
||||
motion: (Point, Point),
|
||||
) -> Vec<Position> {
|
||||
let (accepted, accepted_index) = accepted;
|
||||
let (candidate, candidate_index) = candidate;
|
||||
let (start, pointer) = motion;
|
||||
if ![
|
||||
requested.x,
|
||||
requested.y,
|
||||
start.x,
|
||||
start.y,
|
||||
pointer.x,
|
||||
pointer.y,
|
||||
]
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
let (Some(original), Some(base), Some(anchor)) = (
|
||||
accepted.diagrams.get(&diagram),
|
||||
candidate.diagrams.get(&diagram),
|
||||
affected.get(&anchor),
|
||||
) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let snapped = Position {
|
||||
x: (requested.x / GRID).round() * GRID,
|
||||
y: (requested.y / GRID).round() * GRID,
|
||||
};
|
||||
let delta = Position {
|
||||
x: snapped.x - anchor.position.x,
|
||||
y: snapped.y - anchor.position.y,
|
||||
};
|
||||
let before = projection::project_diagram_with_index(accepted, original, accepted_index);
|
||||
let before_nodes: BTreeMap<_, _> = before.nodes.iter().map(|node| (node.id, node)).collect();
|
||||
let mut sources = BTreeSet::new();
|
||||
for node in before
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| affected.contains_key(&node.id))
|
||||
{
|
||||
let mut parent = node.visual_parent;
|
||||
let mut seen = BTreeSet::new();
|
||||
while let Some(id) = parent {
|
||||
if !seen.insert(id) {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(node) = before_nodes.get(&id) else {
|
||||
break;
|
||||
};
|
||||
if !affected.contains_key(&id)
|
||||
&& accepted
|
||||
.elements
|
||||
.get(&node.element)
|
||||
.is_some_and(|element| element.kind().is_performer())
|
||||
{
|
||||
sources.insert(id);
|
||||
}
|
||||
parent = node.visual_parent;
|
||||
}
|
||||
}
|
||||
if sources.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
// Projection after responsibility removal supplies the former performer's
|
||||
// remaining enclosure, including other allocated work. Projecting the moved
|
||||
// block also includes the full visible descendant enclosure of every root.
|
||||
let mut moved = base.clone();
|
||||
for (id, captured) in affected {
|
||||
let Some(node) = moved.nodes.get_mut(id) else {
|
||||
return Vec::new();
|
||||
};
|
||||
node.position = Position {
|
||||
x: captured.position.x + delta.x,
|
||||
y: captured.position.y + delta.y,
|
||||
};
|
||||
if ![
|
||||
node.position.x,
|
||||
node.position.y,
|
||||
node.size.width,
|
||||
node.size.height,
|
||||
]
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
let after = projection::project_diagram_with_index(candidate, &moved, candidate_index);
|
||||
let mut footprint: Option<Bounds> = None;
|
||||
for node in after
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| affected.contains_key(&node.id))
|
||||
{
|
||||
let Some(bounds) = Bounds::node(node) else {
|
||||
return Vec::new();
|
||||
};
|
||||
footprint = Some(footprint.map_or(bounds, |previous| previous.union(bounds)));
|
||||
}
|
||||
let Some(footprint) = footprint else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut overlaps = Vec::new();
|
||||
for node in after.nodes.iter().filter(|node| sources.contains(&node.id)) {
|
||||
let (Some(old), Some(current)) = (Bounds::node(before_nodes[&node.id]), Bounds::node(node))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
if footprint.overlaps(current) {
|
||||
overlaps.push((old, current));
|
||||
}
|
||||
}
|
||||
if overlaps.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
// Each entry is a half-plane actually crossed by the pointer. At a corner,
|
||||
// prefer the boundary reached first on the press-to-pointer segment. The
|
||||
// direction index is a stable tie-break, independent of placement UUIDs.
|
||||
let mut crossings = [None::<f32>; 4];
|
||||
for (old, _) in &overlaps {
|
||||
for (direction, initial, final_, boundary, outward) in [
|
||||
(
|
||||
0,
|
||||
start.x,
|
||||
pointer.x,
|
||||
old.left,
|
||||
pointer.x < old.left && start.x >= old.left,
|
||||
),
|
||||
(
|
||||
1,
|
||||
start.x,
|
||||
pointer.x,
|
||||
old.right,
|
||||
pointer.x >= old.right && start.x < old.right,
|
||||
),
|
||||
(
|
||||
2,
|
||||
start.y,
|
||||
pointer.y,
|
||||
old.top,
|
||||
pointer.y < old.top && start.y >= old.top,
|
||||
),
|
||||
(
|
||||
3,
|
||||
start.y,
|
||||
pointer.y,
|
||||
old.bottom,
|
||||
pointer.y >= old.bottom && start.y < old.bottom,
|
||||
),
|
||||
] {
|
||||
if outward {
|
||||
let fraction = (boundary - initial) / (final_ - initial);
|
||||
if fraction.is_finite() && (0. ..=1.).contains(&fraction) {
|
||||
crossings[direction] =
|
||||
Some(crossings[direction].map_or(fraction, |old| old.min(fraction)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut directions: Vec<_> = crossings
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(direction, fraction)| fraction.map(|fraction| (direction, fraction)))
|
||||
.collect();
|
||||
directions.sort_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)));
|
||||
let mut result = Vec::new();
|
||||
for (direction, _) in directions {
|
||||
let mut corrected = snapped;
|
||||
match direction {
|
||||
0 => {
|
||||
let edge = overlaps
|
||||
.iter()
|
||||
.map(|(_, current)| current.left)
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
corrected.x = ((snapped.x + edge - GAP - footprint.right) / GRID).floor() * GRID;
|
||||
}
|
||||
1 => {
|
||||
let edge = overlaps
|
||||
.iter()
|
||||
.map(|(_, current)| current.right)
|
||||
.fold(f32::NEG_INFINITY, f32::max);
|
||||
corrected.x = ((snapped.x + edge + GAP - footprint.left) / GRID).ceil() * GRID;
|
||||
}
|
||||
2 => {
|
||||
let edge = overlaps
|
||||
.iter()
|
||||
.map(|(_, current)| current.top)
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
corrected.y = ((snapped.y + edge - GAP - footprint.bottom) / GRID).floor() * GRID;
|
||||
}
|
||||
3 => {
|
||||
let edge = overlaps
|
||||
.iter()
|
||||
.map(|(_, current)| current.bottom)
|
||||
.fold(f32::NEG_INFINITY, f32::max);
|
||||
corrected.y = ((snapped.y + edge + GAP - footprint.top) / GRID).ceil() * GRID;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if corrected.x.is_finite() && corrected.y.is_finite() && !result.contains(&corrected) {
|
||||
result.push(corrected);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::allocation_drag as plan;
|
||||
use oa_core::{Kind, Layer, ProjectionKind, RelationKind, Size};
|
||||
|
||||
struct Fixture {
|
||||
project: Project,
|
||||
diagram: Id,
|
||||
performer: Id,
|
||||
work: Id,
|
||||
node: Id,
|
||||
}
|
||||
|
||||
fn xy(x: f32, y: f32) -> Position {
|
||||
Position { x, y }
|
||||
}
|
||||
|
||||
fn fixture(layer: Layer) -> Fixture {
|
||||
let mut project = Project::empty();
|
||||
let (performer, work) = match layer {
|
||||
Layer::Oa => (Kind::Entity, Kind::Activity),
|
||||
Layer::Sa => (Kind::System, Kind::Function),
|
||||
};
|
||||
let performer = project
|
||||
.add_element(performer, layer, "Performer", None)
|
||||
.unwrap();
|
||||
let work = project.add_element(work, layer, "Work", None).unwrap();
|
||||
let diagram = project.ensure_diagram(layer);
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Allocation;
|
||||
let performer = project.place(diagram, performer, xy(100., 100.));
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&performer)
|
||||
.unwrap()
|
||||
.size = Size {
|
||||
width: 400.,
|
||||
height: 250.,
|
||||
};
|
||||
let node = project.place(diagram, work, xy(160., 180.));
|
||||
let owner = project.diagrams[&diagram].nodes[&performer].element;
|
||||
project.relate(RelationKind::Allocation, "Responsibility", owner, work);
|
||||
Fixture {
|
||||
project,
|
||||
diagram,
|
||||
performer,
|
||||
work,
|
||||
node,
|
||||
}
|
||||
}
|
||||
|
||||
fn semantic(f: &Fixture) -> (plan::Scope, plan::SemanticPlan) {
|
||||
let scope = plan::capture(&f.project, f.diagram, f.work, f.node).unwrap();
|
||||
let semantic = plan::plan_semantics(
|
||||
&f.project,
|
||||
&scope,
|
||||
plan::Destination::OutsidePerformers,
|
||||
&BTreeMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
(scope, semantic)
|
||||
}
|
||||
|
||||
fn presentation() -> plan::PresentationContext {
|
||||
plan::PresentationContext {
|
||||
viewport: plan::Rect {
|
||||
position: xy(-2000., -2000.),
|
||||
size: Size {
|
||||
width: 5000.,
|
||||
height: 5000.,
|
||||
},
|
||||
},
|
||||
zoom: 1.,
|
||||
target: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_outgoing_edge_clears_the_source_and_passes_ordinary_placement() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let f = fixture(layer);
|
||||
let (scope, semantic) = semantic(&f);
|
||||
for (start, pointer, requested, expected) in [
|
||||
(
|
||||
Point::new(170., 210.),
|
||||
Point::new(99., 210.),
|
||||
xy(89., 180.),
|
||||
xy(-140., 180.),
|
||||
),
|
||||
(
|
||||
Point::new(340., 210.),
|
||||
Point::new(501., 210.),
|
||||
xy(321., 180.),
|
||||
xy(510., 180.),
|
||||
),
|
||||
(
|
||||
Point::new(200., 190.),
|
||||
Point::new(200., 99.),
|
||||
xy(160., 89.),
|
||||
xy(160., -10.),
|
||||
),
|
||||
(
|
||||
Point::new(200., 260.),
|
||||
Point::new(200., 351.),
|
||||
xy(160., 271.),
|
||||
xy(160., 360.),
|
||||
),
|
||||
] {
|
||||
assert!(matches!(
|
||||
plan::place(&semantic, requested, presentation()),
|
||||
Err(plan::Refusal::LayoutRequired(_))
|
||||
));
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
requested,
|
||||
(start, pointer),
|
||||
);
|
||||
assert_eq!(proposed, vec![expected]);
|
||||
let placed = plan::place(&semantic, proposed[0], presentation()).unwrap();
|
||||
assert!(
|
||||
plan::finish(&semantic, &f.project, &placed)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_shrink_uses_candidate_enclosure_not_frozen_target() {
|
||||
let mut f = fixture(Layer::Oa);
|
||||
f.project
|
||||
.diagrams
|
||||
.get_mut(&f.diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&f.performer)
|
||||
.unwrap()
|
||||
.size = Size {
|
||||
width: 224.,
|
||||
height: 94.,
|
||||
};
|
||||
let (scope, semantic) = semantic(&f);
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
xy(229., 180.),
|
||||
(Point::new(340., 210.), Point::new(409., 210.)),
|
||||
);
|
||||
assert_eq!(proposed, vec![xy(340., 180.)]);
|
||||
plan::place(&semantic, proposed[0], presentation()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_order_follows_boundary_crossing_and_is_deterministic() {
|
||||
let f = fixture(Layer::Sa);
|
||||
let (scope, semantic) = semantic(&f);
|
||||
// Right crossed at 160/161 of the segment; bottom at 90/91.
|
||||
let args = (
|
||||
xy(321., 271.),
|
||||
Point::new(340., 260.),
|
||||
Point::new(501., 351.),
|
||||
);
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
args.0,
|
||||
(args.1, args.2),
|
||||
);
|
||||
assert_eq!(proposed, vec![xy(320., 360.), xy(510., 270.)]);
|
||||
assert_eq!(
|
||||
proposed,
|
||||
requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
args.0,
|
||||
(args.1, args.2)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_crossing_no_overlap_no_source_and_nonfinite_requests_are_empty() {
|
||||
let f = fixture(Layer::Oa);
|
||||
let (scope, semantic) = semantic(&f);
|
||||
for (requested, pointer) in [
|
||||
(xy(170., 180.), Point::new(210., 210.)),
|
||||
(xy(700., 180.), Point::new(740., 210.)),
|
||||
(xy(f32::NAN, 180.), Point::new(99., 210.)),
|
||||
(xy(89., 180.), Point::new(f32::INFINITY, 210.)),
|
||||
] {
|
||||
assert!(
|
||||
requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
requested,
|
||||
(Point::new(200., 210.), pointer)
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
requests(
|
||||
(semantic.project(), semantic.index()),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
xy(89., 180.),
|
||||
(Point::new(200., 210.), Point::new(99., 210.))
|
||||
)
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descendant_footprint_controls_clearance_without_mutating_projects() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = fixture(layer);
|
||||
let kind = f.project.elements[&f.work].kind();
|
||||
let child = f
|
||||
.project
|
||||
.add_element(kind, layer, "Child", Some(f.work))
|
||||
.unwrap();
|
||||
let child_node = f.project.place(f.diagram, child, xy(240., 270.));
|
||||
// First-child allocation transfer is normally performed by commands;
|
||||
// this fixture expresses the resulting leaf allocation directly.
|
||||
f.project.relations.clear();
|
||||
let owner = f.project.diagrams[&f.diagram].nodes[&f.performer].element;
|
||||
f.project.relate(
|
||||
RelationKind::Allocation,
|
||||
"Leaf responsibility",
|
||||
owner,
|
||||
child,
|
||||
);
|
||||
let (scope, semantic) = semantic(&f);
|
||||
assert!(scope.placements.contains_key(&child_node));
|
||||
let before = f.project.clone();
|
||||
let candidate = semantic.project().clone();
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(&candidate, semantic.index()),
|
||||
f.diagram,
|
||||
&scope.placements,
|
||||
f.node,
|
||||
xy(89., 180.),
|
||||
(Point::new(170., 210.), Point::new(99., 210.)),
|
||||
);
|
||||
assert_eq!(proposed, vec![xy(-240., 180.)]);
|
||||
let placed = plan::place(&semantic, proposed[0], presentation()).unwrap();
|
||||
let root = &placed.diagram.nodes[&f.node];
|
||||
let child = &placed.diagram.nodes[&child_node];
|
||||
assert_eq!(child.position.x - root.position.x, 80.);
|
||||
assert_eq!(child.position.y - root.position.y, 90.);
|
||||
assert_eq!(f.project, before);
|
||||
assert_eq!(semantic.project(), &candidate);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independent_selected_roots_keep_one_rigid_group_translation() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = fixture(layer);
|
||||
let kind = f.project.elements[&f.work].kind();
|
||||
let second = f
|
||||
.project
|
||||
.add_element(kind, layer, "Second work", None)
|
||||
.unwrap();
|
||||
let second_node = f.project.place(f.diagram, second, xy(240., 300.));
|
||||
let owner = f.project.diagrams[&f.diagram].nodes[&f.performer].element;
|
||||
f.project.relate(
|
||||
RelationKind::Allocation,
|
||||
"Second responsibility",
|
||||
owner,
|
||||
second,
|
||||
);
|
||||
let selected = BTreeMap::from([(f.node, f.work), (second_node, second)]);
|
||||
let scope =
|
||||
plan::assignment::capture(&f.project, f.diagram, &selected, f.node).unwrap();
|
||||
let semantic = plan::assignment::plan_semantics(
|
||||
&scope,
|
||||
plan::Destination::OutsidePerformers,
|
||||
&BTreeMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
let proposed = requests(
|
||||
(&f.project, &ViewIndex::new(&f.project)),
|
||||
(semantic.project(), semantic.index()),
|
||||
f.diagram,
|
||||
scope.affected(),
|
||||
f.node,
|
||||
xy(89., 180.),
|
||||
(Point::new(170., 210.), Point::new(99., 210.)),
|
||||
);
|
||||
assert_eq!(proposed, vec![xy(-220., 180.)]);
|
||||
let placed = plan::assignment::place(&semantic, proposed[0], presentation()).unwrap();
|
||||
let root = &placed.diagram.nodes[&f.node];
|
||||
let second = &placed.diagram.nodes[&second_node];
|
||||
assert_eq!(second.position.x - root.position.x, 80.);
|
||||
assert_eq!(second.position.y - root.position.y, 120.);
|
||||
assert!(
|
||||
plan::assignment::finish(&semantic, &f.project, &placed)
|
||||
.unwrap()
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,117 @@ async fn mount() -> (Fixture, Mounted) {
|
||||
mounted.settle(&mut f.app).await;
|
||||
(f, mounted)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn actual_tree_outgoing_boundary_motion_preview_release_and_cancel() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = outgoing_fixture(layer);
|
||||
f.app.layer = layer;
|
||||
f.app.window_size = iced::Size::new(1440., 900.);
|
||||
let _ = f.app.update(Message::Select(f.work));
|
||||
let mut mounted = Mounted::new(&mut f.app).await;
|
||||
f.app.zoom = 0.75;
|
||||
f.app.pan = Vector::new(20., 15.);
|
||||
f.app.fit_to_view = false;
|
||||
mounted.settle(&mut f.app).await;
|
||||
let original = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let from = at(&mut mounted, &f, xy(660., 225.));
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[
|
||||
Input::Mouse(mouse::Event::CursorMoved { position: from }),
|
||||
Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
],
|
||||
mouse::Cursor::Available(from),
|
||||
)
|
||||
.await;
|
||||
// Separate input/update/layout turns expose the held boundary transition
|
||||
// that a one-batch distant drop cannot exercise.
|
||||
for x in [620., 560., 510., 499., 490.] {
|
||||
let to = at(&mut mounted, &f, xy(x, 225.));
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[Input::Mouse(mouse::Event::CursorMoved { position: to })],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(f.app.project(), &original);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(
|
||||
!f.app.allocation_drag.feedback.as_ref().unwrap().refused,
|
||||
"{layer:?} x={x}: {}",
|
||||
f.app.status
|
||||
);
|
||||
assert!(f.app.allocation_drag.preview.is_some());
|
||||
let _ = mounted.pixels(&f.app);
|
||||
}
|
||||
let expected = f
|
||||
.app
|
||||
.allocation_drag
|
||||
.preview
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.placement
|
||||
.diagram
|
||||
.clone();
|
||||
let to = at(&mut mounted, &f, xy(490., 225.));
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[Input::Mouse(mouse::Event::ButtonReleased(
|
||||
mouse::Button::Left,
|
||||
))],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(&f.app.project().diagrams[&f.diagram], &expected);
|
||||
assert!(
|
||||
!f.app
|
||||
.project()
|
||||
.relations
|
||||
.values()
|
||||
.any(|r| r.kind() == RelationKind::Allocation && r.target() == f.work)
|
||||
);
|
||||
let _ = f.app.update(Message::Undo);
|
||||
mounted.settle(&mut f.app).await;
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
let from = at(&mut mounted, &f, xy(660., 225.));
|
||||
let to = at(&mut mounted, &f, xy(490., 225.));
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[
|
||||
Input::Mouse(mouse::Event::CursorMoved { position: from }),
|
||||
Input::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)),
|
||||
Input::Mouse(mouse::Event::CursorMoved { position: to }),
|
||||
],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[key(keyboard::key::Named::Escape)],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
mounted
|
||||
.batch(
|
||||
&mut f.app,
|
||||
&[Input::Mouse(mouse::Event::ButtonReleased(
|
||||
mouse::Button::Left,
|
||||
))],
|
||||
mouse::Cursor::Available(to),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert!(!f.app.document.can_undo());
|
||||
}
|
||||
}
|
||||
fn canvas_bounds(mounted: &mut Mounted, app: &App) -> Rectangle {
|
||||
mounted
|
||||
.geometry(app)
|
||||
@@ -481,6 +592,7 @@ async fn actual_app_tree_exact_incoming_drag_keeps_destination_visible_and_canva
|
||||
if pan.y == -100. {
|
||||
// A genuine clipping refusal in the same held gesture, then
|
||||
// recovery at the original exact point: no camera rebasing.
|
||||
let retained = f.app.allocation_drag.preview.clone().unwrap();
|
||||
let clipped = at(&mut mounted, &f, xy(160., 110.));
|
||||
mounted
|
||||
.batch(
|
||||
@@ -492,7 +604,19 @@ async fn actual_app_tree_exact_incoming_drag_keeps_destination_visible_and_canva
|
||||
)
|
||||
.await;
|
||||
assert!(f.app.allocation_drag.feedback.as_ref().unwrap().refused);
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert!(Arc::ptr_eq(
|
||||
f.app.allocation_drag.preview.as_ref().unwrap(),
|
||||
&retained
|
||||
));
|
||||
assert!(
|
||||
f.app
|
||||
.allocation_drag
|
||||
.feedback
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.ghost_delta
|
||||
.is_some()
|
||||
);
|
||||
assert_eq!(canvas_bounds(&mut mounted, &f.app), canvas);
|
||||
assert_eq!(destination_bounds(&mut mounted, &f.app), slot);
|
||||
assert_eq!(f.app.project(), &before);
|
||||
|
||||
@@ -1239,6 +1239,7 @@ fn destination_identity_does_not_describe_blocked_or_outside_viewport_as_unalloc
|
||||
inspection_targets: vec![],
|
||||
text: "A card blocks this drop".into(),
|
||||
refused: true,
|
||||
ghost_delta: None,
|
||||
};
|
||||
assert_eq!(feedback.destination_identity(), "No eligible destination");
|
||||
feedback.text = "Release inside the captured canvas".into();
|
||||
@@ -1246,3 +1247,361 @@ fn destination_identity_does_not_describe_blocked_or_outside_viewport_as_unalloc
|
||||
feedback.refused = false;
|
||||
assert_eq!(feedback.destination_identity(), "Outside performers");
|
||||
}
|
||||
|
||||
fn outgoing_fixture(layer: Layer) -> Fixture {
|
||||
let mut f = fixture(layer);
|
||||
drag(&mut f, screen(xy(160., 190.)));
|
||||
let mut project = f.app.project().clone();
|
||||
let d = project.diagrams.get_mut(&f.diagram).unwrap();
|
||||
let performer = d.nodes.values_mut().find(|n| n.element == f.a).unwrap();
|
||||
performer.position = xy(500., 100.);
|
||||
performer.size = Size {
|
||||
width: 300.,
|
||||
height: 300.,
|
||||
};
|
||||
d.nodes.get_mut(&f.node).unwrap().position = xy(540., 190.);
|
||||
replace(&mut f, project);
|
||||
f
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outgoing_boundary_drop_clears_source_without_changing_other_placements() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = outgoing_fixture(layer);
|
||||
let before = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let from = screen(xy(660., 225.));
|
||||
let to = screen(xy(490., 225.));
|
||||
let mut events = batch(&f.app, from, to);
|
||||
let release = events.pop().unwrap();
|
||||
send(&mut f.app, events);
|
||||
let feedback = f.app.allocation_drag.feedback.as_ref().unwrap();
|
||||
assert!(!feedback.refused, "{}", feedback.text);
|
||||
let preview = f.app.allocation_drag.preview.as_ref().unwrap();
|
||||
let expected_diagram = preview.placement.diagram.clone();
|
||||
let work = &expected_diagram.nodes[&f.node];
|
||||
assert!(work.position.x + work.size.width <= 490.);
|
||||
assert_eq!(work.position.y, 190.);
|
||||
for (id, node) in &before.diagrams[&f.diagram].nodes {
|
||||
if *id != f.node {
|
||||
assert_eq!(&expected_diagram.nodes[id], node);
|
||||
}
|
||||
}
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
let _ = f.app.update(release);
|
||||
assert_eq!(&f.app.project().diagrams[&f.diagram], &expected_diagram);
|
||||
assert!(
|
||||
!f.app
|
||||
.project()
|
||||
.relations
|
||||
.values()
|
||||
.any(|r| r.kind() == RelationKind::Allocation && r.target() == f.work)
|
||||
);
|
||||
let after = f.app.project().clone();
|
||||
let saved = f.app.document.bundle().clone();
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(!f.app.document.can_undo());
|
||||
let _ = f.app.update(Message::Redo);
|
||||
assert_eq!(f.app.project(), &after);
|
||||
assert_eq!(f.app.document.bundle(), &saved);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outgoing_clearance_collision_refuses_release_without_source_or_history_mutation() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = outgoing_fixture(layer);
|
||||
let mut project = f.app.project().clone();
|
||||
let obstacle = project
|
||||
.add_element(
|
||||
project.elements[&f.work].kind(),
|
||||
layer,
|
||||
"Unrelated work",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let obstacle_node = project.place(f.diagram, obstacle, xy(240., 190.));
|
||||
replace(&mut f, project);
|
||||
// A refused release must preserve an existing redo branch, too.
|
||||
let _ = f.app.update(Message::Command(Command::Rename(
|
||||
f.work,
|
||||
"Temporary".into(),
|
||||
)));
|
||||
let _ = f.app.update(Message::Undo);
|
||||
let before = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let snapshot = f.app.document.snapshot();
|
||||
let revision = f.app.document.revision();
|
||||
let from = screen(xy(660., 225.));
|
||||
let to = screen(xy(490., 225.));
|
||||
let mut events = batch(&f.app, from, to);
|
||||
let release = events.pop().unwrap();
|
||||
send(&mut f.app, events);
|
||||
|
||||
let active = f.app.allocation_drag.active.as_ref().unwrap();
|
||||
let p = &active.press;
|
||||
let frozen = active.frozen.as_ref().unwrap();
|
||||
let owner = frozen.targets.iter().find(|t| t.element == f.a).unwrap();
|
||||
assert_eq!(p.start, from);
|
||||
assert!(contains(owner.region, p.stamp.world(from)));
|
||||
assert!(p.stamp.world(to).x < owner.region.x);
|
||||
assert_eq!(
|
||||
frozen.resolve(p.stamp, Some(to), false),
|
||||
Hit::Destination(plan::Destination::OutsidePerformers, None),
|
||||
"The pointer must reach empty canvas, not be occluded by the obstacle"
|
||||
);
|
||||
let candidate = active
|
||||
.cache
|
||||
.iter()
|
||||
.find(|(destination, _)| *destination == plan::Destination::OutsidePerformers)
|
||||
.unwrap()
|
||||
.1
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
let affected = match &active.prepared {
|
||||
Prepared::Singleton(scope) => &scope.placements,
|
||||
Prepared::Assignment(scope) => scope.affected(),
|
||||
_ => panic!("expected assignment gesture"),
|
||||
};
|
||||
let requested = xy(370., 190.);
|
||||
let presentation = plan::PresentationContext {
|
||||
viewport: p.stamp.viewport(),
|
||||
zoom: p.stamp.zoom,
|
||||
target: None,
|
||||
};
|
||||
assert!(matches!(
|
||||
candidate.place(requested, presentation),
|
||||
Err(plan::Refusal::LayoutRequired(_))
|
||||
));
|
||||
let proposed = clearance::requests(
|
||||
(f.app.project(), f.app.document.view_index()),
|
||||
(candidate.project(), candidate.index()),
|
||||
f.diagram,
|
||||
affected,
|
||||
f.node,
|
||||
requested,
|
||||
(p.stamp.world(from), p.stamp.world(to)),
|
||||
);
|
||||
assert_eq!(
|
||||
proposed.len(),
|
||||
1,
|
||||
"a genuine left crossing must propose clearance"
|
||||
);
|
||||
assert!(proposed[0].x < requested.x);
|
||||
let work = &before.diagrams[&f.diagram].nodes[&f.node];
|
||||
let obstacle = &before.diagrams[&f.diagram].nodes[&obstacle_node];
|
||||
assert!(obstacle.position.x + obstacle.size.width < 490.);
|
||||
assert!(proposed[0].x + work.size.width <= owner.region.x - 10.);
|
||||
assert!(proposed[0].x < obstacle.position.x + obstacle.size.width);
|
||||
assert!(obstacle.position.x < proposed[0].x + work.size.width);
|
||||
assert_eq!(proposed[0].y, obstacle.position.y);
|
||||
assert!(matches!(
|
||||
candidate.place(proposed[0], presentation),
|
||||
Err(plan::Refusal::LayoutRequired(message)) if message.contains("overlapping cards")
|
||||
));
|
||||
let feedback = f.app.allocation_drag.feedback.as_ref().unwrap();
|
||||
assert!(feedback.refused, "{}", feedback.text);
|
||||
assert!(feedback.ghost_delta.is_some());
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
||||
let _ = f.app.update(release);
|
||||
assert!(
|
||||
f.app.status.contains("overlapping cards"),
|
||||
"{}",
|
||||
f.app.status
|
||||
);
|
||||
assert!(f.app.allocation_drag.active.is_none());
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert_eq!(f.app.document.revision(), revision);
|
||||
assert!(Arc::ptr_eq(&snapshot, &f.app.document.snapshot()));
|
||||
assert!(!f.app.document.can_undo());
|
||||
assert!(f.app.document.can_redo());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn outgoing_clearance_preserves_source_enclosure_and_allocated_sibling() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
let mut f = outgoing_fixture(layer);
|
||||
let mut project = f.app.project().clone();
|
||||
let sibling = project
|
||||
.add_element(
|
||||
project.elements[&f.work].kind(),
|
||||
layer,
|
||||
"Remaining work",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let sibling_node = project.place(f.diagram, sibling, xy(540., 450.));
|
||||
let allocation = project.relate(
|
||||
RelationKind::Allocation,
|
||||
"Keep responsibility",
|
||||
f.a,
|
||||
sibling,
|
||||
);
|
||||
replace(&mut f, project);
|
||||
let before = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let view = projection::project_view(&before, f.diagram);
|
||||
let owner = view.nodes.iter().find(|n| n.element == f.a).unwrap();
|
||||
let bottom = owner.position.y + owner.size.height;
|
||||
let stored_owner = &before.diagrams[&f.diagram].nodes[&owner.id];
|
||||
assert!(
|
||||
bottom > stored_owner.position.y + stored_owner.size.height,
|
||||
"The retained sibling must enlarge the projected source enclosure"
|
||||
);
|
||||
let from = screen(xy(660., 225.));
|
||||
let to = screen(xy(660., bottom + 1.));
|
||||
let mut events = batch(&f.app, from, to);
|
||||
let release = events.pop().unwrap();
|
||||
send(&mut f.app, events);
|
||||
let active = f.app.allocation_drag.active.as_ref().unwrap();
|
||||
let p = &active.press;
|
||||
let frozen = active.frozen.as_ref().unwrap();
|
||||
let target = frozen.targets.iter().find(|t| t.element == f.a).unwrap();
|
||||
assert_eq!(p.start, from);
|
||||
assert!(contains(target.region, p.stamp.world(from)));
|
||||
assert!(p.stamp.world(to).y >= target.region.y + target.region.height);
|
||||
assert_eq!(
|
||||
frozen.resolve(p.stamp, Some(to), false),
|
||||
Hit::Destination(plan::Destination::OutsidePerformers, None)
|
||||
);
|
||||
let preview = f.app.allocation_drag.preview.as_ref().expect(&f.app.status);
|
||||
assert!(!f.app.allocation_drag.feedback.as_ref().unwrap().refused);
|
||||
let requested = xy(540., 190. + to.y - from.y);
|
||||
assert!(matches!(
|
||||
preview.candidate.place(
|
||||
requested,
|
||||
plan::PresentationContext {
|
||||
viewport: p.stamp.viewport(),
|
||||
zoom: p.stamp.zoom,
|
||||
target: None,
|
||||
}
|
||||
),
|
||||
Err(plan::Refusal::LayoutRequired(_))
|
||||
));
|
||||
let moved = &preview.placement.diagram.nodes[&f.node];
|
||||
assert_eq!(moved.position.x, 540.);
|
||||
assert!(moved.position.y >= bottom + 10.);
|
||||
assert!(moved.position.y > requested.y);
|
||||
assert_eq!(
|
||||
preview
|
||||
.placement
|
||||
.view
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.id == owner.id)
|
||||
.unwrap(),
|
||||
owner
|
||||
);
|
||||
let remaining = preview
|
||||
.placement
|
||||
.view
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|n| n.id == sibling_node)
|
||||
.unwrap();
|
||||
assert_eq!(remaining.visual_parent, Some(owner.id));
|
||||
assert_eq!(
|
||||
remaining.position,
|
||||
before.diagrams[&f.diagram].nodes[&sibling_node].position
|
||||
);
|
||||
assert_eq!(
|
||||
&preview.project().relations[&allocation],
|
||||
&before.relations[&allocation]
|
||||
);
|
||||
for (id, node) in &before.diagrams[&f.diagram].nodes {
|
||||
if *id != f.node {
|
||||
assert_eq!(&preview.placement.diagram.nodes[id], node);
|
||||
}
|
||||
}
|
||||
let mut expected = preview.project().clone();
|
||||
expected
|
||||
.diagrams
|
||||
.insert(f.diagram, preview.placement.diagram.clone());
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
let _ = f.app.update(release);
|
||||
assert_eq!(f.app.project(), &expected, "{}", f.app.status);
|
||||
assert_eq!(f.app.document.revision(), 1);
|
||||
assert_eq!(
|
||||
validation::effective_performer(f.app.project(), f.work),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
validation::effective_performer(f.app.project(), sibling),
|
||||
Some(f.a)
|
||||
);
|
||||
assert_eq!(
|
||||
&f.app.project().relations[&allocation],
|
||||
&before.relations[&allocation]
|
||||
);
|
||||
let after = f.app.document.bundle().clone();
|
||||
let _ = f.app.update(Message::Undo);
|
||||
assert_eq!(f.app.project(), &before);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(!f.app.document.can_undo());
|
||||
let _ = f.app.update(Message::Redo);
|
||||
assert_eq!(f.app.project(), &expected);
|
||||
assert_eq!(f.app.document.bundle(), &after);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refused_motion_retains_last_scene_but_release_never_commits_it() {
|
||||
let mut f = outgoing_fixture(Layer::Sa);
|
||||
let mut project = f.app.project().clone();
|
||||
let obstacle = project
|
||||
.add_element(Kind::Function, Layer::Sa, "Unrelated work", None)
|
||||
.unwrap();
|
||||
project.place(f.diagram, obstacle, xy(1250., 120.));
|
||||
replace(&mut f, project);
|
||||
let original = f.app.project().clone();
|
||||
let source = f.app.document.bundle().clone();
|
||||
let p = admit_press(&mut f);
|
||||
let clear = screen(xy(450., 225.));
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Preview(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(clear),
|
||||
)));
|
||||
let last = f.app.allocation_drag.preview.clone().expect(&f.app.status);
|
||||
// Another work card blocks this location: the prior legal outgoing scene is display
|
||||
// state only, and must not become a fallback command on release.
|
||||
let blocked = screen(xy(1260., 130.));
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Preview(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(blocked),
|
||||
)));
|
||||
assert!(f.app.allocation_drag.feedback.as_ref().unwrap().refused);
|
||||
assert!(
|
||||
f.app
|
||||
.allocation_drag
|
||||
.feedback
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.ghost_delta
|
||||
.is_some()
|
||||
);
|
||||
assert!(Arc::ptr_eq(
|
||||
f.app.allocation_drag.preview.as_ref().unwrap(),
|
||||
&last
|
||||
));
|
||||
let _ = f.app.update(Message::WorkDrag(Event::Release(
|
||||
p.stamp,
|
||||
p.token,
|
||||
Some(blocked),
|
||||
)));
|
||||
assert_eq!(f.app.project(), &original);
|
||||
assert_eq!(f.app.document.bundle(), &source);
|
||||
assert!(!f.app.document.can_undo());
|
||||
assert!(f.app.allocation_drag.preview.is_none());
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ async fn press_escape(mounted: &mut Mounted, app: &mut App) -> Vec<Message> {
|
||||
std::slice::from_ref(&input),
|
||||
mouse::Cursor::Unavailable,
|
||||
);
|
||||
assert_eq!(
|
||||
statuses,
|
||||
[iced::event::Status::Captured],
|
||||
"the mounted picker must capture Escape before App subscriptions"
|
||||
);
|
||||
crate::app::escape::tests::subscriptions(app, input, statuses[0], &mut messages);
|
||||
for message in messages.iter().cloned() {
|
||||
let task = app.update(message);
|
||||
@@ -41,17 +46,11 @@ async fn mounted_state_missing_picker_escape_preserves_exact_document_then_reope
|
||||
send(&mut app, Message::BehaviorDiagram(choice));
|
||||
let mut mounted = Mounted::new(&mut app).await;
|
||||
assert_eq!(state_edit::unplaced(app.project(), diagram), vec![f.b]);
|
||||
let focus_id = iced::advanced::widget::Id::from("diagram-keyboard-owner");
|
||||
let (_, canvas, _) = mounted
|
||||
.geometry(&app)
|
||||
.focused
|
||||
.into_iter()
|
||||
.find(|(id, _, _)| id.as_ref() == Some(&focus_id))
|
||||
.unwrap();
|
||||
// The existing picker is immediately above the measured canvas, separated
|
||||
// by 8px. Its 12px text and 8px padding give a 31.6px row.
|
||||
let picker = iced::Point::new(canvas.center_x(), canvas.y - 24.);
|
||||
let option = iced::Point::new(picker.x, canvas.y + 8.);
|
||||
// Locate the stable picker slot itself: selection controls may sit between
|
||||
// it and the canvas. The single menu option opens below this slot.
|
||||
let bounds = mounted.geometry(&app).bounds("state-missing-picker");
|
||||
let picker = bounds.center();
|
||||
let option = iced::Point::new(picker.x, bounds.y + bounds.height * 1.5);
|
||||
let before = app.document.snapshot();
|
||||
let original = app.project().clone();
|
||||
let source = app.document.bundle().clone();
|
||||
|
||||
@@ -187,7 +187,7 @@ pub fn view<'a>(app: &'a App, machine: Id, mut out: Column<'a, Message>) -> Elem
|
||||
.into();
|
||||
out = out.push(
|
||||
column![
|
||||
missing_controls,
|
||||
container(missing_controls).id("state-missing-picker"),
|
||||
selection_controls(app),
|
||||
crate::diagram::focus::wrap(
|
||||
canvas.map(event),
|
||||
|
||||
@@ -42,6 +42,7 @@ mod port_labels;
|
||||
pub use port_labels::PortLabels;
|
||||
#[path = "diagram/focus.rs"]
|
||||
pub(crate) mod focus;
|
||||
mod grid;
|
||||
pub(crate) mod group_gesture;
|
||||
mod multiselect;
|
||||
#[cfg(test)]
|
||||
@@ -71,6 +72,7 @@ pub struct DiagramCanvas<'a> {
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
grid: grid::Grid,
|
||||
modifiers: iced::keyboard::Modifiers,
|
||||
group: Option<multiselect::Press>,
|
||||
work: Option<Arc<crate::app::allocation_drag::Press>>,
|
||||
@@ -1251,23 +1253,10 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
bounds: Rectangle,
|
||||
_: mouse::Cursor,
|
||||
) -> Vec<canvas::Geometry> {
|
||||
let background = state
|
||||
.grid
|
||||
.draw(renderer, bounds.size(), self.zoom, self.pan);
|
||||
let mut f = Frame::new(renderer, bounds.size());
|
||||
f.fill_rectangle(
|
||||
Point::ORIGIN,
|
||||
bounds.size(),
|
||||
Color::from_rgb8(246, 248, 251),
|
||||
);
|
||||
let step = (24. * self.zoom).max(14.);
|
||||
let ox = self.pan.x.rem_euclid(step);
|
||||
let oy = self.pan.y.rem_euclid(step);
|
||||
for x in 0..(bounds.width / step) as i32 + 1 {
|
||||
for y in 0..(bounds.height / step) as i32 + 1 {
|
||||
f.fill(
|
||||
&Path::circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65),
|
||||
style::LINE,
|
||||
);
|
||||
}
|
||||
}
|
||||
f.translate(self.pan);
|
||||
f.scale(self.zoom);
|
||||
let scene = self.scene(state);
|
||||
@@ -1670,7 +1659,7 @@ impl canvas::Program<Message> for DiagramCanvas<'_> {
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
vec![f.into_geometry(), overlay.into_geometry()]
|
||||
vec![background, f.into_geometry(), overlay.into_geometry()]
|
||||
}
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
//! Screen-space background geometry, independent of the model and pointer.
|
||||
use super::*;
|
||||
use std::cell::Cell;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Grid {
|
||||
geometry: canvas::Cache,
|
||||
camera: Cell<Option<(f32, Vector)>>,
|
||||
#[cfg(test)]
|
||||
builds: Cell<usize>,
|
||||
}
|
||||
|
||||
impl Grid {
|
||||
pub(super) fn draw(
|
||||
&self,
|
||||
renderer: &Renderer,
|
||||
size: Size,
|
||||
zoom: f32,
|
||||
pan: Vector,
|
||||
) -> canvas::Geometry {
|
||||
// Cache::draw handles exact size changes. Raw camera equality is
|
||||
// deliberately conservative: do not quantize fractional antialiasing.
|
||||
if self.camera.replace(Some((zoom, pan))) != Some((zoom, pan)) {
|
||||
self.geometry.clear();
|
||||
}
|
||||
self.geometry.draw(renderer, size, |frame| {
|
||||
#[cfg(test)]
|
||||
self.builds.set(self.builds.get() + 1);
|
||||
paint(frame, size, zoom, pan);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(frame: &mut Frame, size: Size, zoom: f32, pan: Vector) {
|
||||
frame.fill_rectangle(Point::ORIGIN, size, Color::from_rgb8(246, 248, 251));
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
frame.fill(
|
||||
&Path::circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65),
|
||||
style::LINE,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,378 @@
|
||||
use super::*;
|
||||
use iced::advanced::{
|
||||
graphics::geometry::Renderer as _,
|
||||
renderer::{Headless, Renderer as _},
|
||||
};
|
||||
|
||||
async fn renderer() -> Renderer {
|
||||
<Renderer as Headless>::new(iced::Font::DEFAULT, 16.into(), None)
|
||||
.await
|
||||
.expect("grid tests require the supported WGPU headless renderer")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cache_reuses_unchanged_inputs_and_rebuilds_for_each_camera_or_size_change() {
|
||||
let renderer = renderer().await;
|
||||
let grid = Grid::default();
|
||||
let original = Size::new(320., 240.);
|
||||
let threshold = 14. / 24.;
|
||||
// Each row changes exactly one dependency from its predecessor. Returning
|
||||
// to old dimensions must invalidate too; this cache holds one viewport.
|
||||
let cases = [
|
||||
("initial", original, 1., Vector::ZERO),
|
||||
("positive x pan", original, 1., Vector::new(24., 0.)),
|
||||
("negative x pan", original, 1., Vector::new(-24., 0.)),
|
||||
("fractional x pan", original, 1., Vector::new(-24.25, 0.)),
|
||||
("positive y pan", original, 1., Vector::new(-24.25, 18.)),
|
||||
("negative y pan", original, 1., Vector::new(-24.25, -18.)),
|
||||
("fractional y pan", original, 1., Vector::new(-24.25, -18.5)),
|
||||
("return to zero pan", original, 1., Vector::ZERO),
|
||||
("zoom at zero pan", original, 1.25, Vector::ZERO),
|
||||
(
|
||||
"below spacing threshold",
|
||||
original,
|
||||
threshold - 0.001,
|
||||
Vector::ZERO,
|
||||
),
|
||||
("at spacing threshold", original, threshold, Vector::ZERO),
|
||||
(
|
||||
"above spacing threshold",
|
||||
original,
|
||||
threshold + 0.001,
|
||||
Vector::ZERO,
|
||||
),
|
||||
(
|
||||
"width only",
|
||||
Size::new(321., 240.),
|
||||
threshold + 0.001,
|
||||
Vector::ZERO,
|
||||
),
|
||||
("return width", original, threshold + 0.001, Vector::ZERO),
|
||||
(
|
||||
"height only",
|
||||
Size::new(320., 241.),
|
||||
threshold + 0.001,
|
||||
Vector::ZERO,
|
||||
),
|
||||
("return height", original, threshold + 0.001, Vector::ZERO),
|
||||
(
|
||||
"fractional width",
|
||||
Size::new(320.25, 240.),
|
||||
threshold + 0.001,
|
||||
Vector::ZERO,
|
||||
),
|
||||
(
|
||||
"fractional height",
|
||||
Size::new(320.25, 240.5),
|
||||
threshold + 0.001,
|
||||
Vector::ZERO,
|
||||
),
|
||||
];
|
||||
for (index, (label, size, zoom, pan)) in cases.into_iter().enumerate() {
|
||||
let _ = grid.draw(&renderer, size, zoom, pan);
|
||||
assert_eq!(grid.builds.get(), index + 1, "{label} must rebuild");
|
||||
for _ in 0..3 {
|
||||
let _ = grid.draw(&renderer, size, zoom, pan);
|
||||
assert_eq!(grid.builds.get(), index + 1, "{label} must reuse");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Independent reference copied from the former DiagramCanvas::draw background
|
||||
// pass. Do not call grid::paint here: a shared implementation would hide changes
|
||||
// in spacing, phase, radius, color, or coverage from the parity comparison.
|
||||
fn legacy_background(frame: &mut Frame, size: Size, zoom: f32, pan: Vector) {
|
||||
frame.fill_rectangle(Point::ORIGIN, size, Color::from_rgb8(246, 248, 251));
|
||||
let spacing = (24. * zoom).max(14.);
|
||||
let offset_x = pan.x.rem_euclid(spacing);
|
||||
let offset_y = pan.y.rem_euclid(spacing);
|
||||
for column in 0..(size.width / spacing) as i32 + 1 {
|
||||
for row in 0..(size.height / spacing) as i32 + 1 {
|
||||
frame.fill(
|
||||
&Path::circle(
|
||||
Point::new(
|
||||
offset_x + column as f32 * spacing,
|
||||
offset_y + row as f32 * spacing,
|
||||
),
|
||||
0.65,
|
||||
),
|
||||
Color::from_rgb(0.86, 0.89, 0.91),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn foreground(frame: &mut Frame, zoom: f32, pan: Vector, changed: bool) {
|
||||
frame.translate(pan);
|
||||
frame.scale(zoom);
|
||||
// Crossing paths pass behind opaque cards. The long paths also cross the
|
||||
// screen-space warning, making geometry order observable.
|
||||
for (start, end, color) in [
|
||||
(Point::new(0., 20.), Point::new(300., 210.), style::TEAL),
|
||||
(Point::new(0., 210.), Point::new(300., 20.), style::BOUNDARY),
|
||||
] {
|
||||
frame.stroke(
|
||||
&Path::line(start, end),
|
||||
Stroke::default().with_color(color).with_width(3.),
|
||||
);
|
||||
}
|
||||
let shift = if changed { 19.5 } else { 0. };
|
||||
frame.fill_rectangle(
|
||||
Point::new(12., 92.),
|
||||
Size::new(270., 35.),
|
||||
Color::from_rgba(0.1, 0.6, 0.5, 0.25),
|
||||
);
|
||||
for (position, size, title) in [
|
||||
(Point::new(32. + shift, 58.), Size::new(116., 72.), "Sensor"),
|
||||
(Point::new(172., 139.), Size::new(118., 66.), "Controller"),
|
||||
] {
|
||||
frame.fill_rectangle(position, size, Color::WHITE);
|
||||
frame.stroke(
|
||||
&Path::rectangle(position, size),
|
||||
Stroke::default().with_color(style::BOUNDARY).with_width(1.),
|
||||
);
|
||||
frame.fill_text(Text {
|
||||
content: title.into(),
|
||||
position: position + Vector::new(8., 12.),
|
||||
color: style::INK,
|
||||
size: 13.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
// Selection paint and handles remain live foreground data, including when
|
||||
// the underlying background cache is reused after an edit.
|
||||
frame.stroke(
|
||||
&Path::rectangle(Point::new(29. + shift, 55.), Size::new(122., 78.)),
|
||||
Stroke::default().with_color(style::TEAL).with_width(2.),
|
||||
);
|
||||
frame.fill_rectangle(Point::new(26. + shift, 52.), Size::new(6., 6.), style::TEAL);
|
||||
frame.fill_text(Text {
|
||||
content: if changed {
|
||||
"Edited connection"
|
||||
} else {
|
||||
"Connection"
|
||||
}
|
||||
.into(),
|
||||
position: Point::new(116., 113.),
|
||||
color: style::INK,
|
||||
size: 11.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
fn warning(frame: &mut Frame, size: Size) {
|
||||
frame.fill_rectangle(
|
||||
Point::new(8., 8.),
|
||||
Size::new((size.width - 16.).max(0.), 30.),
|
||||
Color::WHITE,
|
||||
);
|
||||
frame.fill_text(Text {
|
||||
content: "1 connection route needs attention".into(),
|
||||
position: Point::new(16., 15.),
|
||||
color: style::MUTED,
|
||||
size: 12.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
fn legacy_geometry(
|
||||
renderer: &Renderer,
|
||||
size: Size,
|
||||
zoom: f32,
|
||||
pan: Vector,
|
||||
changed: bool,
|
||||
) -> Vec<canvas::Geometry> {
|
||||
let mut combined = Frame::new(renderer, size);
|
||||
legacy_background(&mut combined, size, zoom, pan);
|
||||
foreground(&mut combined, zoom, pan, changed);
|
||||
let mut overlay = Frame::new(renderer, size);
|
||||
warning(&mut overlay, size);
|
||||
vec![combined.into_geometry(), overlay.into_geometry()]
|
||||
}
|
||||
|
||||
fn cached_geometry(
|
||||
grid: &Grid,
|
||||
renderer: &Renderer,
|
||||
size: Size,
|
||||
zoom: f32,
|
||||
pan: Vector,
|
||||
changed: bool,
|
||||
) -> Vec<canvas::Geometry> {
|
||||
let background = grid.draw(renderer, size, zoom, pan);
|
||||
let mut content = Frame::new(renderer, size);
|
||||
foreground(&mut content, zoom, pan, changed);
|
||||
let mut overlay = Frame::new(renderer, size);
|
||||
warning(&mut overlay, size);
|
||||
vec![background, content.into_geometry(), overlay.into_geometry()]
|
||||
}
|
||||
|
||||
fn pixels(renderer: &mut Renderer, size: Size, geometry: Vec<canvas::Geometry>) -> Vec<u8> {
|
||||
// Keep physical screenshot dimensions and renderer viewport consistent even
|
||||
// for fractional canvas bounds; the geometry itself keeps its exact size.
|
||||
let physical = Size::new(size.width.ceil() as u32, size.height.ceil() as u32);
|
||||
renderer.reset(Rectangle::with_size(Size::new(
|
||||
physical.width as f32,
|
||||
physical.height as f32,
|
||||
)));
|
||||
for layer in geometry {
|
||||
renderer.draw_geometry(layer);
|
||||
}
|
||||
let pixels = Headless::screenshot(renderer, physical, 1., style::BG);
|
||||
assert_eq!(
|
||||
pixels.len(),
|
||||
physical.width as usize * physical.height as usize * 4
|
||||
);
|
||||
pixels
|
||||
}
|
||||
|
||||
fn assert_same_pixels(expected: &[u8], actual: &[u8], label: &str) {
|
||||
assert_eq!(expected.len(), actual.len(), "{label}: dimensions");
|
||||
let differing = expected
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.zip(actual.as_chunks::<4>().0)
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
assert_eq!(differing, 0, "{label}: changed RGBA pixels");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cached_background_preserves_pixels_and_live_foreground_across_camera_and_size_changes() {
|
||||
let mut renderer = renderer().await;
|
||||
let grid = Grid::default();
|
||||
let original = Size::new(320., 240.);
|
||||
let threshold = 14. / 24.;
|
||||
for (label, size, zoom, pan) in [
|
||||
("origin", original, 1., Vector::ZERO),
|
||||
("positive pan", original, 1., Vector::new(17., 11.)),
|
||||
("negative pan", original, 1., Vector::new(-17., -11.)),
|
||||
("fractional pan", original, 1., Vector::new(-17.25, 11.5)),
|
||||
("zoom zero pan", original, 1.25, Vector::ZERO),
|
||||
("below threshold", original, threshold - 0.001, Vector::ZERO),
|
||||
("at threshold", original, threshold, Vector::ZERO),
|
||||
("above threshold", original, threshold + 0.001, Vector::ZERO),
|
||||
("width resize", Size::new(327., 240.), 1., Vector::ZERO),
|
||||
("height resize", Size::new(327., 247.), 1., Vector::ZERO),
|
||||
("return size", original, 1., Vector::ZERO),
|
||||
(
|
||||
"fractional bounds",
|
||||
Size::new(319.25, 239.5),
|
||||
1.,
|
||||
Vector::new(-0.5, 0.25),
|
||||
),
|
||||
] {
|
||||
let old = legacy_geometry(&renderer, size, zoom, pan, false);
|
||||
let expected = pixels(&mut renderer, size, old);
|
||||
let layers = cached_geometry(&grid, &renderer, size, zoom, pan, false);
|
||||
let actual = pixels(&mut renderer, size, layers);
|
||||
assert_same_pixels(&expected, &actual, label);
|
||||
let builds = grid.builds.get();
|
||||
let reused = cached_geometry(&grid, &renderer, size, zoom, pan, false);
|
||||
let reused = pixels(&mut renderer, size, reused);
|
||||
assert_same_pixels(&expected, &reused, label);
|
||||
assert_eq!(grid.builds.get(), builds, "{label}: screenshot reuse");
|
||||
|
||||
let edited_old = legacy_geometry(&renderer, size, zoom, pan, true);
|
||||
let edited_expected = pixels(&mut renderer, size, edited_old);
|
||||
assert_ne!(
|
||||
expected, edited_expected,
|
||||
"{label}: edit must visibly change foreground"
|
||||
);
|
||||
let edited = cached_geometry(&grid, &renderer, size, zoom, pan, true);
|
||||
let edited = pixels(&mut renderer, size, edited);
|
||||
assert_same_pixels(&edited_expected, &edited, label);
|
||||
assert_eq!(
|
||||
grid.builds.get(),
|
||||
builds,
|
||||
"{label}: foreground edit must reuse grid"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn actual_canvas_keeps_background_cached_while_selection_and_revision_change() {
|
||||
use super::super::{Context, DiagramCanvas, State};
|
||||
let mut renderer = renderer().await;
|
||||
let mut project = Project::empty();
|
||||
let a = project
|
||||
.add_element(Kind::Entity, Layer::Oa, "Sensor", None)
|
||||
.unwrap();
|
||||
let b = project
|
||||
.add_element(Kind::Actor, Layer::Oa, "Controller", None)
|
||||
.unwrap();
|
||||
let diagram = project.ensure_diagram(Layer::Oa);
|
||||
project.diagrams.get_mut(&diagram).unwrap().projection = ProjectionKind::Overview;
|
||||
let node = project.place(diagram, a, Position { x: 20., y: 50. });
|
||||
project.place(diagram, b, Position { x: 100., y: 160. });
|
||||
project.relate(RelationKind::CommunicationMean, "Link", a, b);
|
||||
let mut canvas = DiagramCanvas {
|
||||
selection: None,
|
||||
allocation: None,
|
||||
project: &project,
|
||||
view_index: None,
|
||||
diagram,
|
||||
selected: a,
|
||||
navigation_focus: None,
|
||||
zoom: 0.75,
|
||||
pan: Vector::ZERO,
|
||||
context: Context {
|
||||
document: project.id,
|
||||
diagram,
|
||||
revision: 0,
|
||||
epoch: 0,
|
||||
},
|
||||
selected_bend: None,
|
||||
selected_attachment: None,
|
||||
editable: true,
|
||||
placing: false,
|
||||
connection: None,
|
||||
};
|
||||
let state = State::default();
|
||||
let size = Size::new(360., 280.);
|
||||
let bounds = Rectangle::with_size(size);
|
||||
let draw = |canvas: &DiagramCanvas<'_>, renderer: &Renderer, bounds| {
|
||||
canvas::Program::draw(
|
||||
canvas,
|
||||
&state,
|
||||
renderer,
|
||||
&crate::style::theme(),
|
||||
bounds,
|
||||
mouse::Cursor::Unavailable,
|
||||
)
|
||||
};
|
||||
let layers = draw(&canvas, &renderer, bounds);
|
||||
assert_eq!(layers.len(), 3, "background, scene, warning overlay");
|
||||
let original = pixels(&mut renderer, size, layers);
|
||||
assert_eq!(state.grid.builds.get(), 1);
|
||||
canvas.selected = b;
|
||||
let layers = draw(&canvas, &renderer, bounds);
|
||||
let selected = pixels(&mut renderer, size, layers);
|
||||
assert_ne!(original, selected, "selection must remain live");
|
||||
assert_eq!(state.grid.builds.get(), 1);
|
||||
let mut changed = project.clone();
|
||||
changed
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&node)
|
||||
.unwrap()
|
||||
.position
|
||||
.x += 30.;
|
||||
canvas.project = &changed;
|
||||
canvas.context.revision += 1;
|
||||
let layers = draw(&canvas, &renderer, bounds);
|
||||
let moved = pixels(&mut renderer, size, layers);
|
||||
assert_ne!(selected, moved, "new model revision must remain live");
|
||||
assert_eq!(state.grid.builds.get(), 1);
|
||||
canvas.pan.x = -0.25;
|
||||
let _ = draw(&canvas, &renderer, bounds);
|
||||
assert_eq!(state.grid.builds.get(), 2);
|
||||
let _ = draw(
|
||||
&canvas,
|
||||
&renderer,
|
||||
Rectangle::with_size(Size::new(361., 280.)),
|
||||
);
|
||||
assert_eq!(state.grid.builds.get(), 3);
|
||||
}
|
||||
@@ -243,6 +243,21 @@ impl canvas::Program<Message> for WorkFeedback<'_> {
|
||||
let Some(frozen) = &feedback.frozen else {
|
||||
return vec![];
|
||||
};
|
||||
if let Some(delta) = feedback.ghost_delta {
|
||||
for region in &frozen.moving {
|
||||
let top = Point::new(
|
||||
(region.x + delta.x) * self.0.zoom + self.0.pan.x,
|
||||
(region.y + delta.y) * self.0.zoom + self.0.pan.y,
|
||||
);
|
||||
frame.stroke(
|
||||
&Path::rectangle(
|
||||
top,
|
||||
iced::Size::new(region.width * self.0.zoom, region.height * self.0.zoom),
|
||||
),
|
||||
Stroke::default().with_color(color).with_width(2.),
|
||||
);
|
||||
}
|
||||
}
|
||||
for index in &feedback.targets {
|
||||
let target = &frozen.targets[*index];
|
||||
let region = target.region;
|
||||
|
||||
+13
-1
@@ -2702,7 +2702,19 @@ performer change with diagram placement. The preview MUST identify the
|
||||
destination and preserve readable work and destination identity. Destination
|
||||
identity is shown outside the canvas, independently of explanatory or refusal
|
||||
text. The outlined target's hit region remains fixed throughout the gesture;
|
||||
feedback MUST NOT obscure the work or change its pointer-relative placement.
|
||||
feedback MUST NOT obscure the work. Preserve pointer-relative placement when
|
||||
the requested position is valid. When an outgoing drop would leave work
|
||||
overlapping its former performer, the editor MAY propose a minimal grid-aligned
|
||||
translation through a crossed performer boundary, with one grid unit of
|
||||
clearance. It MUST preview that exact position, translate the complete affected
|
||||
work group rigidly, and recheck all semantic and collision constraints on release.
|
||||
It MUST NOT move unrelated placements or search arbitrary free space. If no
|
||||
proposal is valid, a pointer-following outline MAY accompany the last valid
|
||||
preview; that retained preview MUST NOT authorize a refused release.
|
||||
Outgoing work headers MAY be partly outside the viewport provided each retains
|
||||
at least a 24-by-24 logical-pixel grab region. Releasing the pointer outside the
|
||||
viewport still cancels. Other assignment and Arrange gestures retain their
|
||||
complete-header visibility requirement.
|
||||
A clean click selects without applying unrelated drafts. A refused work press
|
||||
with an unapplied property draft MUST preserve its text, caret and focused
|
||||
editor. An accepted drag is one atomic model/source/history transaction:
|
||||
|
||||
+13
-1
@@ -2128,7 +2128,19 @@ performer change with diagram placement. The preview MUST identify the
|
||||
destination and preserve readable work and destination identity. Destination
|
||||
identity is shown outside the canvas, independently of explanatory or refusal
|
||||
text. The outlined target's hit region remains fixed throughout the gesture;
|
||||
feedback MUST NOT obscure the work or change its pointer-relative placement.
|
||||
feedback MUST NOT obscure the work. Preserve pointer-relative placement when
|
||||
the requested position is valid. When an outgoing drop would leave work
|
||||
overlapping its former performer, the editor MAY propose a minimal grid-aligned
|
||||
translation through a crossed performer boundary, with one grid unit of
|
||||
clearance. It MUST preview that exact position, translate the complete affected
|
||||
work group rigidly, and recheck all semantic and collision constraints on release.
|
||||
It MUST NOT move unrelated placements or search arbitrary free space. If no
|
||||
proposal is valid, a pointer-following outline MAY accompany the last valid
|
||||
preview; that retained preview MUST NOT authorize a refused release.
|
||||
Outgoing work headers MAY be partly outside the viewport provided each retains
|
||||
at least a 24-by-24 logical-pixel grab region. Releasing the pointer outside the
|
||||
viewport still cancels. Other assignment and Arrange gestures retain their
|
||||
complete-header visibility requirement.
|
||||
A clean click selects without applying unrelated drafts. A refused work press
|
||||
with an unapplied property draft MUST preserve its text, caret and focused
|
||||
editor. An accepted drag is one atomic model/source/history transaction:
|
||||
|
||||
+7
-1
@@ -25,7 +25,13 @@ the current code includes that one-line correction. The original compile failed
|
||||
before running tests. The first combined compile at `897d9f04` subsequently
|
||||
found a missing `text_editor` import in the Allocation Text Apply regression;
|
||||
the current source includes that one-line correction too. Both failures remain
|
||||
recorded in the source handoff. Corrected combined execution remains pending.
|
||||
recorded in the source handoff. The subsequent full test run at `8ddfadf4`
|
||||
failed the mounted State picker fixture: added selection controls invalidated
|
||||
its canvas-relative clicks. Source successor `a9cd364eacbbffa0693bab05b5a2b65f9d105535`
|
||||
measures an identified fixed-height picker slot and checks Escape capture,
|
||||
retaining the exact preservation/reopen/Place/Undo assertions. The source
|
||||
handoff links the preserved failed and interrupted attempts. Corrected combined
|
||||
Windows execution and package acceptance remain pending.
|
||||
|
||||
Live build, final-source and publication results belong in
|
||||
[issue37](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/37).
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Outgoing allocation dragging
|
||||
|
||||
Scope: [issue 40](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/40),
|
||||
based on published Windows source `715036e132677dd2bc2fb37ed316d93dc9987aec`.
|
||||
This contribution is not integrated release acceptance. User requirements remain
|
||||
in `PROJECT_GOAL.md`; this note records a revision to derived interaction policy.
|
||||
|
||||
## Reproduction and cause
|
||||
|
||||
The released executable SHA256
|
||||
`2c03bd6cc3f1110c219e4f57ab94874a121590f83779239a93289b7170664399`
|
||||
opened a copied Allocation/Process companion fixture on Windows. At 1440x900,
|
||||
72% Fit, the OA assign view's X-receive was dragged from window (486,546) to
|
||||
(365,546), just beyond A's left edge. It remained assigned at its original
|
||||
position, reporting enlarged card overlap. A control drag to (625,572) removed
|
||||
assignment; Undo restored the original scene. These are actual native end-state
|
||||
observations; intermediate jitter was diagnosed from source, not frame capture.
|
||||
|
||||
Frozen targets resolve from the pointer, while placement retains its grab
|
||||
offset. The pointer therefore leaves its performer before the full work card
|
||||
does. Former parent/child intersection becomes unrelated overlap after removing
|
||||
allocation. Refused motion previously cleared the preview and redrew the saved
|
||||
scene. Additionally, whole-header viewport containment rejects a useful outgoing
|
||||
position in the fitted reproduction even when its model geometry is valid.
|
||||
|
||||
## Selected policy
|
||||
|
||||
Try exact snapped pointer placement first. Only an outgoing assignment candidate
|
||||
with a placement refusal may propose bounded source clearance: move the entire
|
||||
captured work footprint through a crossed frozen source boundary, clearing the
|
||||
post-removal source enclosure by one 10-world-unit grid cell. Consider at most
|
||||
four directions, ordered by press-to-pointer boundary-crossing fraction and a
|
||||
fixed direction tie-break. Existing placement, collision, semantic, exact-command
|
||||
and source/history admission remain authoritative. No neighbor is moved, no
|
||||
general packing/search occurs, and no semantic refusal is overridden.
|
||||
|
||||
The accepted/candidate semantic indexes are reused during fallback projections.
|
||||
Outgoing headers may be partly clipped but each must retain a 24x24 logical-pixel
|
||||
grab region. Other assignment and Arrange visibility rules remain unchanged.
|
||||
The exact corrected candidate is previewed; release recomputes it. Blocked
|
||||
in-bounds motion retains its last admitted scene and adds an orange pointer
|
||||
outline. This display state confers no permission to commit on refused release.
|
||||
Cancel, outside release and gesture invalidation retire it normally.
|
||||
|
||||
Rejected alternatives: retaining a moving outline alone leaves the boundary
|
||||
dead band; allowing unrelated overlap makes unassigned work look allocated;
|
||||
general auto-layout changes unrelated placement and makes drag intent harder to
|
||||
predict. The previous unconditional pointer-offset rule is narrowed in
|
||||
`docs/language.rst` and its HTML copy. Real neighboring-card collisions and
|
||||
insufficient visible grab area can still refuse a drop.
|
||||
|
||||
## Verification status
|
||||
|
||||
Independent source diagnosis and repair critique found no semantic/stale/cancel
|
||||
bypass. Review exposed the remaining whole-header camera restriction and an
|
||||
obsolete retained-preview assertion; both were revised. Added checks cover
|
||||
OA/SA boundary clearance, corners, source shrinkage, rigid descendants/groups,
|
||||
real App frame-by-frame input, exact source/history, cancellation, blocked release
|
||||
and outgoing grab-area boundaries. At clean source
|
||||
`1a669130fb6d2b65279aab3847e966a1a282c191`, the Windows allocation-drag
|
||||
suite passes 121 tests (none failed or ignored; 1,424 filtered out), strict
|
||||
desktop all-target Clippy passes, and the normal desktop development build
|
||||
passes. [Raw results and reproduction commands](experiments/drag-out-20260925/README.md)
|
||||
preserve the source, platform, failed attempts and exact scope. These checks do
|
||||
not prove native usability. Computer Use was stopped with the physical Escape
|
||||
key before the changed executable could be inspected. Fitted-view dragging,
|
||||
clipped-card identification/regrabbing, native Undo/Redo and Save/reopen remain
|
||||
unverified. The issue remains open; no integration or release acceptance follows
|
||||
from the targeted gates.
|
||||
|
||||
Test-only successor `24b1eab07d306d8cb091bb46f7daf9f027a8f1c0` additionally
|
||||
checks an actual fallback blocked by unrelated work, including preservation of
|
||||
an existing redo branch, and an outgoing move while an allocated sibling keeps
|
||||
the source enclosure expanded. Both run for OA and SA. All 123 allocation-drag
|
||||
tests and strict desktop Clippy pass at that successor. Fresh independent review
|
||||
confirms those tests exercise their claimed invariants, with no material source
|
||||
finding in this scope. The same linked evidence includes the new logs and review;
|
||||
production code and native-acceptance limits remain unchanged.
|
||||
|
||||
The first build attempt failed before tests because the sparse checkout omitted
|
||||
`build/windows_manifest.rs`; restoring the tracked build directory corrected the
|
||||
checkout. The local drive initially had about 22 MB free. Git sparse checkout and
|
||||
Cargo's package-scoped cleanup of inactive desktop artifacts recovered capacity.
|
||||
Automatic review rejected recursive deletion of the extracted preview; it was
|
||||
left intact. No published package or unique evidence was deleted.
|
||||
The second attempt also exposed omitted tracked harness fixtures and a `Self`
|
||||
typo in a standalone test helper; both were corrected before the passing run.
|
||||
That failed compiler process was terminated after its errors were captured.
|
||||
@@ -0,0 +1 @@
|
||||
*.log -text
|
||||
@@ -0,0 +1,91 @@
|
||||
# Small architecture diagram CPU investigation
|
||||
|
||||
Question: does core projection/routing explain [issue 44's low FPS](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/44)
|
||||
with roughly eight architecture cards? This experiment narrows that question;
|
||||
it neither reproduces native frame rate nor establishes a complete cause.
|
||||
|
||||
## Workload and source
|
||||
|
||||
The screenshot uses names from `examples/release1`. Its exact model, projection,
|
||||
camera and authored geometry are unavailable. The probe loads the unchanged
|
||||
`examples/release1/model.json`, measures its five saved views, and constructs a
|
||||
clearly labeled eight-card SA Overview approximation using those semantic
|
||||
elements. Approximate coordinates and stable placement IDs are in the probe;
|
||||
it has 8 projected nodes, 6 ports and 13 projected edges. It is not a recovered
|
||||
copy of the user's diagram. All data changes occur in memory.
|
||||
|
||||
Source: `24b1eab07d306d8cb091bb46f7daf9f027a8f1c0`; core, manifest and lockfile
|
||||
are unchanged from the drag contribution's `1a669130` production source.
|
||||
Windows 10.0.26200, Rust 1.98.1, x86_64-pc-windows-gnullvm, LLVM-MinGW
|
||||
20260908 UCRT, reported CPU Intel Core i7-10870H with 4 exposed cores/threads.
|
||||
The probe was optimized with `-O` and linked to the existing **unoptimized
|
||||
development-profile core library** used by the completed desktop test build.
|
||||
This distinction matters: optimizing the driver does not optimize that library.
|
||||
The desktop test process had completed before measurement; no Cargo compilation
|
||||
was started until after measurement. Other operating-system workloads were not
|
||||
controlled, so outliers are retained rather than interpreted as stable capacity.
|
||||
|
||||
## Method and results
|
||||
|
||||
Each phase gets 3 warmups and 40 timed samples, with returned values passed to
|
||||
`black_box`. Reported p50/p95 select sorted index `ceil((n-1)*p)` as implemented
|
||||
in the source. Returned output destruction is outside each timed phase. Diagram
|
||||
movement uses a deterministic small delta sequence and the existing descendant
|
||||
and authored-route translation APIs. Index creation, projection, routing, and
|
||||
combined movement preparation are measured separately. The probe does not model
|
||||
allocation transaction admission or an Iced frame.
|
||||
|
||||
For the eight-card approximation, `performance-01.log` records:
|
||||
|
||||
| Phase | p50 ms | p95 ms | max ms |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| Reused-index projection | 0.3885 | 0.7550 | 1.2965 |
|
||||
| Fresh-index projection | 1.7135 | 2.7022 | 4.2204 |
|
||||
| Settled routing | 2.4525 | 4.4617 | 4.7735 |
|
||||
| Moved routing | 3.1907 | 4.4781 | 4.5672 |
|
||||
| Movement preparation + projection + routing | 4.2640 | 6.8259 | 8.8052 |
|
||||
|
||||
This workload does not expose severe core latency sufficient by itself to
|
||||
explain very low FPS. It does not rule out different authored routes, allocation
|
||||
admission, hardware/rendering, or the user's exact scene. Repeated fresh-index
|
||||
projection has a measurable cost, but no change is justified solely by this
|
||||
small sample. No FPS or performance-fix claim follows.
|
||||
|
||||
## Source observations and next discriminating measurement
|
||||
|
||||
Independent read-only hot-path review found that `DiagramCanvas::scene` already
|
||||
caches projection and routing by semantic context and transient geometry.
|
||||
Ordinary node movement changes preview geometry and routes the full view;
|
||||
route gestures use `route_edge_preview`. Ordinary hover need not reroute.
|
||||
|
||||
`diagram/port_labels.rs` requests redraw on every mouse event, including when
|
||||
no tooltip is visible. `DiagramCanvas::draw` rebuilds geometry and each dot-grid
|
||||
circle on every draw. At a 1900x660 canvas this yields about 2,240 circles at
|
||||
zoom 1 and 6,528 at minimum spacing. Scene caching does not cache that geometry.
|
||||
`views.rs` also performs an unindexed `project_view` during app view rebuilding;
|
||||
legacy node-motion redraw does not itself require that full UI rebuild.
|
||||
|
||||
Next, measure actual canvas draw/frame costs independently for idle hover, pan,
|
||||
ordinary node drag and allocation drag, with grid generation separated from
|
||||
node/edge/text rendering and frame presentation. Compare the same scene/camera
|
||||
and renderer before changing production code. Native automation was stopped
|
||||
with Escape; no GPU/frame measurement was attempted in this experiment. Live
|
||||
ownership and subsequent results belong to issue 44.
|
||||
|
||||
## Reproduction
|
||||
|
||||
Build `oa-core` at the identified source with the stated Windows toolchain, then
|
||||
compile this standalone probe against that exact library and matching dependency
|
||||
directory. For a fresh standard Cargo development build:
|
||||
|
||||
```powershell
|
||||
cargo build --locked -p oa-core
|
||||
rustc --edition=2024 -O harness/experiments/architecture-perf-20260925/architecture_perf_probe.rs --extern oa_core=target/debug/liboa_core.rlib -L dependency=target/debug/deps -C linker=clang.exe -o architecture_perf_probe.exe
|
||||
./architecture_perf_probe.exe examples/release1/model.json 40
|
||||
```
|
||||
|
||||
The actual run linked `target/debug/deps/liboa_core-2f3c5174cb6398fd.rlib`
|
||||
and used the same dependency directory. Set the linker/runtime PATH as required
|
||||
by the Windows toolchain. Compiler flags, core profile, machine load and fixture
|
||||
all affect timings; preserve them when comparing candidates. This standalone
|
||||
probe is experimental evidence, not a production dependency or a timing test.
|
||||
@@ -0,0 +1,166 @@
|
||||
//! Local-only CPU diagnosis. Does not measure native FPS, canvas redraw, or GPU.
|
||||
//! Usage: architecture_perf_probe.exe PATH_TO_RELEASE1_MODEL_JSON [SAMPLES=40]
|
||||
use oa_core::{persistence, projection, routing, view_options::ViewIndex, *};
|
||||
use std::{collections::BTreeMap, hint::black_box, time::Instant};
|
||||
|
||||
fn measure<T>(label: &str, samples: usize, mut f: impl FnMut(usize) -> T) {
|
||||
for i in 0..3 {
|
||||
black_box(f(i));
|
||||
}
|
||||
let mut ms = Vec::with_capacity(samples);
|
||||
for i in 0..samples {
|
||||
let start = Instant::now();
|
||||
let output = black_box(f(i));
|
||||
ms.push(start.elapsed().as_secs_f64() * 1000.0);
|
||||
drop(output); // Exclude destruction consistently from each timed phase.
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
let percentile = |p: f64| ms[((samples - 1) as f64 * p).ceil() as usize];
|
||||
println!(
|
||||
"{label}: n={samples} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
percentile(0.5),
|
||||
percentile(0.95),
|
||||
ms[samples - 1]
|
||||
);
|
||||
}
|
||||
|
||||
fn probe(p: &Project, d: &Diagram, samples: usize) {
|
||||
let index = ViewIndex::new(p);
|
||||
let view = projection::project_diagram_with_index(p, d, &index);
|
||||
println!(
|
||||
"\nview={} projection={:?} nodes={} ports={} edges={}",
|
||||
d.name,
|
||||
d.projection,
|
||||
view.nodes.len(),
|
||||
view.ports.len(),
|
||||
view.edges.len()
|
||||
);
|
||||
measure("index_new", samples, |_| ViewIndex::new(black_box(p)));
|
||||
measure("projection_reused_index", samples, |_| {
|
||||
projection::project_diagram_with_index(black_box(p), black_box(d), black_box(&index))
|
||||
});
|
||||
measure("projection_fresh_index", samples, |_| {
|
||||
projection::project_diagram(black_box(p), black_box(d))
|
||||
});
|
||||
measure("routing_settled", samples, |_| {
|
||||
routing::route_diagram(black_box(p), black_box(d), black_box(&view))
|
||||
});
|
||||
// Deterministic slight motion of the first root card. Includes the same
|
||||
// descendant/route translation as legacy single-card canvas drag. This is
|
||||
// presentation-only, not the allocation transaction pipeline.
|
||||
let Some(root) = view.nodes.iter().find(|n| n.visual_parent.is_none()) else {
|
||||
return;
|
||||
};
|
||||
println!("moving_root={} element={}", root.id, p.named(root.element));
|
||||
let mut moved = projection::descendant_nodes(p, d.id, root.id);
|
||||
moved.push(root.id);
|
||||
let make_moved = |i: usize| {
|
||||
let delta = Position {
|
||||
x: (i % 21) as f32 * 3.0,
|
||||
y: (i % 11) as f32 * 2.0,
|
||||
};
|
||||
let mut preview = d.clone();
|
||||
let deltas = moved
|
||||
.iter()
|
||||
.map(|id| (*id, delta))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
for route in projection::translated_routes(p, d, &deltas) {
|
||||
if let Some(stored) = preview.routes.iter_mut().find(|r| r.edge == route.edge) {
|
||||
*stored = route;
|
||||
} else {
|
||||
preview.routes.push(route);
|
||||
}
|
||||
}
|
||||
for id in &moved {
|
||||
if let Some(n) = preview.nodes.get_mut(id) {
|
||||
n.position.x += delta.x;
|
||||
n.position.y += delta.y;
|
||||
}
|
||||
}
|
||||
preview
|
||||
};
|
||||
let previews = (0..samples.max(3)).map(make_moved).collect::<Vec<_>>();
|
||||
let projected = previews
|
||||
.iter()
|
||||
.map(|d| projection::project_diagram_with_index(p, d, &index))
|
||||
.collect::<Vec<_>>();
|
||||
measure("moved_projection", samples, |i| {
|
||||
projection::project_diagram_with_index(p, black_box(&previews[i]), &index)
|
||||
});
|
||||
measure("moved_routing", samples, |i| {
|
||||
routing::route_diagram(p, black_box(&previews[i]), black_box(&projected[i]))
|
||||
});
|
||||
measure("moved_prepare_projection_routing", samples, |i| {
|
||||
let preview = make_moved(i);
|
||||
let view = projection::project_diagram_with_index(p, &preview, &index);
|
||||
routing::route_diagram(p, &preview, &view)
|
||||
});
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = std::env::args().collect::<Vec<_>>();
|
||||
let path = args
|
||||
.get(1)
|
||||
.ok_or("usage: architecture_perf_probe PATH [SAMPLES]")?;
|
||||
let samples = args
|
||||
.get(2)
|
||||
.map(|s| s.parse::<usize>())
|
||||
.transpose()?
|
||||
.unwrap_or(40)
|
||||
.max(1);
|
||||
let p = persistence::decode(&std::fs::read(path)?)?;
|
||||
println!(
|
||||
"CPU-only probe; samples={samples}; fixture={path}; elements={} relations={}",
|
||||
p.elements.len(),
|
||||
p.relations.len()
|
||||
);
|
||||
for d in p.diagrams.values() {
|
||||
probe(&p, d, samples);
|
||||
}
|
||||
// Approximation of the reported eight-card screenshot, not an exact repro:
|
||||
// fixture semantics, Overview projection, synthetic positions/card sizes.
|
||||
let mut approx = p.clone();
|
||||
let id: Id = "f0440000-0000-4000-8000-000000000001".parse()?;
|
||||
approx.diagrams.insert(
|
||||
id,
|
||||
Diagram {
|
||||
id,
|
||||
alias: "fps44_approx_overview".into(),
|
||||
name: "APPROXIMATION eight-card SA Overview".into(),
|
||||
description: "Synthetic positions, not user geometry".into(),
|
||||
layer: Layer::Sa,
|
||||
projection: ProjectionKind::Overview,
|
||||
view_options: Default::default(),
|
||||
nodes: BTreeMap::new(),
|
||||
routes: vec![],
|
||||
layout: None,
|
||||
},
|
||||
);
|
||||
for (ordinal, (alias, x, y)) in [
|
||||
("sa_assessment_procedure", 30., 240.),
|
||||
("sa_triage", 510., 240.),
|
||||
("sa_control", 990., 240.),
|
||||
("sa_technician", 530., 440.),
|
||||
("sa_assess", 1490., 240.),
|
||||
("sa_repair", 1030., 440.),
|
||||
("sa_confirm", 1530., 440.),
|
||||
("sa_reset", 30., 440.),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let element = approx
|
||||
.elements
|
||||
.values()
|
||||
.find(|e| e.alias == alias)
|
||||
.ok_or_else(|| format!("missing alias {alias}"))?
|
||||
.id;
|
||||
let generated = approx.place(id, element, Position { x, y });
|
||||
let diagram = approx.diagrams.get_mut(&id).unwrap();
|
||||
let mut node = diagram.nodes.remove(&generated).unwrap();
|
||||
node.id = format!("f0440000-0000-4000-8000-{:012}", ordinal + 2).parse()?;
|
||||
diagram.nodes.insert(node.id, node);
|
||||
}
|
||||
probe(&approx, &approx.diagrams[&id], samples);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"source": "24b1eab07d306d8cb091bb46f7daf9f027a8f1c0",
|
||||
"core_profile": "dev unoptimized",
|
||||
"driver_optimization": "rustc -O",
|
||||
"samples_per_phase": 40,
|
||||
"warmups_per_phase": 3,
|
||||
"rlib_sha256": "E10C81BF28A33CD0FB6C4C26884CFFA480EF05328E78BCDB138DD4B635B2531C",
|
||||
"fixture_sha256": "EF0EF4006C3E09D5DB6E1B621256B3F7D6642F3A7B09CC34607CFBCCA830C727",
|
||||
"driver_exe_sha256": "4C66E9CDE4F08D6DFD2A0DC3DBF1A80B97E340F3F22CA001A76301F06F47C2E2",
|
||||
"native_fps_measured": false
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
CPU-only probe; samples=40; fixture=.local/drag-out-01a0d6fd/examples/release1/model.json; elements=94 relations=62
|
||||
|
||||
view=1 · OA responsibility and repair traffic projection=Allocation nodes=6 ports=0 edges=3
|
||||
index_new: n=40 p50_ms=1.0106 p95_ms=2.0583 max_ms=2.0910
|
||||
projection_reused_index: n=40 p50_ms=0.5702 p95_ms=1.2320 max_ms=1.3490
|
||||
projection_fresh_index: n=40 p50_ms=1.5505 p95_ms=3.6681 max_ms=5.1533
|
||||
routing_settled: n=40 p50_ms=0.1944 p95_ms=0.3391 max_ms=0.4246
|
||||
moving_root=3191e05c-1c7f-5f0e-beb0-5f3bded54c48 element=Control room
|
||||
moved_projection: n=40 p50_ms=0.4848 p95_ms=1.2745 max_ms=1.5797
|
||||
moved_routing: n=40 p50_ms=0.1994 p95_ms=0.7971 max_ms=1.1414
|
||||
moved_prepare_projection_routing: n=40 p50_ms=0.9250 p95_ms=2.6847 max_ms=10.4908
|
||||
|
||||
view=5 · SA pressure supervision projection=StateMachine { machine: 132b3f28-3f88-5327-9868-8c8b645ed412 } nodes=5 ports=0 edges=3
|
||||
index_new: n=40 p50_ms=0.9602 p95_ms=2.1394 max_ms=2.4512
|
||||
projection_reused_index: n=40 p50_ms=0.1697 p95_ms=0.7629 max_ms=1.1780
|
||||
projection_fresh_index: n=40 p50_ms=1.1189 p95_ms=2.6324 max_ms=3.3912
|
||||
routing_settled: n=40 p50_ms=0.7063 p95_ms=1.0812 max_ms=1.1149
|
||||
moving_root=a676fbb9-0833-59a8-98cf-a10d8f4b7434 element=Pressure supervision
|
||||
moved_projection: n=40 p50_ms=0.1565 p95_ms=0.3445 max_ms=0.3790
|
||||
moved_routing: n=40 p50_ms=0.6113 p95_ms=1.3199 max_ms=1.3733
|
||||
moved_prepare_projection_routing: n=40 p50_ms=0.8259 p95_ms=1.7640 max_ms=2.1752
|
||||
|
||||
view=2 · SA service boundary and functions projection=Allocation nodes=6 ports=6 edges=3
|
||||
index_new: n=40 p50_ms=0.7820 p95_ms=3.4180 max_ms=6.8439
|
||||
projection_reused_index: n=40 p50_ms=0.7938 p95_ms=1.9946 max_ms=2.1411
|
||||
projection_fresh_index: n=40 p50_ms=2.0597 p95_ms=3.4554 max_ms=3.6616
|
||||
routing_settled: n=40 p50_ms=0.1478 p95_ms=0.7886 max_ms=1.0081
|
||||
moving_root=8345c76a-8d85-5c31-902e-ec1cd33ed4a3 element=Pressure coordination service
|
||||
moved_projection: n=40 p50_ms=0.6914 p95_ms=1.5286 max_ms=1.8658
|
||||
moved_routing: n=40 p50_ms=0.1862 p95_ms=0.5658 max_ms=2.8924
|
||||
moved_prepare_projection_routing: n=40 p50_ms=1.1724 p95_ms=3.1806 max_ms=4.2620
|
||||
|
||||
view=4 · SA mission and included capability projection=Capabilities nodes=4 ports=1 edges=3
|
||||
index_new: n=40 p50_ms=0.6879 p95_ms=1.3582 max_ms=1.5266
|
||||
projection_reused_index: n=40 p50_ms=0.1562 p95_ms=0.6767 max_ms=1.0102
|
||||
projection_fresh_index: n=40 p50_ms=1.2929 p95_ms=2.3543 max_ms=4.0762
|
||||
routing_settled: n=40 p50_ms=0.2553 p95_ms=1.4389 max_ms=1.6677
|
||||
moving_root=069a3fb3-077c-59d8-91da-27b0665143c7 element=Assess an alarm
|
||||
moved_projection: n=40 p50_ms=0.1243 p95_ms=0.1928 max_ms=0.2212
|
||||
moved_routing: n=40 p50_ms=0.2271 p95_ms=0.6733 max_ms=1.2408
|
||||
moved_prepare_projection_routing: n=40 p50_ms=0.5542 p95_ms=1.3344 max_ms=1.8957
|
||||
|
||||
view=3 · SA service conversation only projection=Allocation nodes=6 ports=6 edges=1
|
||||
index_new: n=40 p50_ms=0.9618 p95_ms=1.6890 max_ms=2.0995
|
||||
projection_reused_index: n=40 p50_ms=0.6661 p95_ms=2.3388 max_ms=2.6429
|
||||
projection_fresh_index: n=40 p50_ms=2.3913 p95_ms=4.1941 max_ms=4.8120
|
||||
routing_settled: n=40 p50_ms=0.0660 p95_ms=0.2156 max_ms=0.6289
|
||||
moving_root=40210589-69f7-5209-b329-10bb83cf615c element=Field technician
|
||||
moved_projection: n=40 p50_ms=0.5527 p95_ms=1.1239 max_ms=1.1362
|
||||
moved_routing: n=40 p50_ms=0.0495 p95_ms=0.1771 max_ms=0.2209
|
||||
moved_prepare_projection_routing: n=40 p50_ms=0.7963 p95_ms=4.1835 max_ms=6.7551
|
||||
|
||||
view=APPROXIMATION eight-card SA Overview projection=Overview nodes=8 ports=6 edges=13
|
||||
index_new: n=40 p50_ms=0.7487 p95_ms=1.9597 max_ms=2.2005
|
||||
projection_reused_index: n=40 p50_ms=0.3885 p95_ms=0.7550 max_ms=1.2965
|
||||
projection_fresh_index: n=40 p50_ms=1.7135 p95_ms=2.7022 max_ms=4.2204
|
||||
routing_settled: n=40 p50_ms=2.4525 p95_ms=4.4617 max_ms=4.7735
|
||||
moving_root=f0440000-0000-4000-8000-000000000002 element=Assess an alarm procedure
|
||||
moved_projection: n=40 p50_ms=0.4691 p95_ms=0.8354 max_ms=1.0408
|
||||
moved_routing: n=40 p50_ms=3.1907 p95_ms=4.4781 max_ms=4.5672
|
||||
moved_prepare_projection_routing: n=40 p50_ms=4.2640 p95_ms=6.8259 max_ms=8.8052
|
||||
@@ -0,0 +1 @@
|
||||
*.log -text
|
||||
@@ -0,0 +1,74 @@
|
||||
# Outgoing allocation drag evidence
|
||||
|
||||
Contribution source: `1a669130fb6d2b65279aab3847e966a1a282c191`, based on
|
||||
published preview `715036e132677dd2bc2fb37ed316d93dc9987aec`.
|
||||
This is branch-specific Windows evidence for [issue 40](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/40).
|
||||
It is not acceptance of an integrated release or the complete application.
|
||||
The [decision and native baseline](../../drag-out-20260925.md) explain the repair.
|
||||
|
||||
## Environment and commands
|
||||
|
||||
Windows 10.0.26200, Rust/Cargo 1.98.1, host/target
|
||||
`x86_64-pc-windows-gnullvm`, LLVM 22.1.8, LLVM-MinGW 20260908 UCRT linker
|
||||
`clang.exe`, `CARGO_BUILD_JOBS=1`, `CARGO_INCREMENTAL=0`, default development
|
||||
profile. Run at the identified source with the Windows GNU LLVM toolchain and
|
||||
its linker/runtime available on PATH. A sparse checkout must include `build`
|
||||
and `harness/fixtures` as well as crates. The actual commands were:
|
||||
|
||||
```powershell
|
||||
cargo test --locked --offline -p oa-desktop allocation_drag -- --test-threads=1
|
||||
cargo clippy --locked --offline -p oa-desktop --all-targets -- -D warnings
|
||||
cargo build --locked --offline -p oa-desktop --bin arcadia-studio
|
||||
```
|
||||
|
||||
Offline commands require dependencies already cached. A fresh environment must
|
||||
fetch the locked dependencies first. Raw logs retain original absolute paths;
|
||||
these paths identify this run, not required directories on another machine.
|
||||
|
||||
## Results and boundaries
|
||||
|
||||
- Test-only successor `24b1eab07d306d8cb091bb46f7daf9f027a8f1c0` adds two
|
||||
OA/SA App regressions: a clearance proposal blocked by unrelated work and an
|
||||
outgoing move with a retained sibling enlarging the source enclosure.
|
||||
`tests-04.log` passes all 123 allocation-drag tests (zero failed/ignored,
|
||||
1,424 filtered) in 54.86s; `clippy-02.log` passes the same strict command in
|
||||
1m27s. Production inputs are unchanged from `1a669130`; its normal binary
|
||||
remains the native candidate. [Independent review](independent-review.md)
|
||||
covers the source, new test design and evidence limits.
|
||||
- `tests-03.log`: 121 passed, zero failed/ignored, 1,424 filtered; 35.33 seconds
|
||||
test execution. This is the allocation-drag subset, not the full desktop suite.
|
||||
- `clippy-01.log`: strict desktop all-target Clippy passed in 4m30s.
|
||||
- `build-01.log`: normal GUI build passed in 5m16s. The resulting local executable
|
||||
SHA256 is `9bda1a6a1d24fdc98c7a8822400c12fea9afe1de828a82ba9273f03b99e47c07`.
|
||||
It was staged with the published preview's `libunwind.dll` and started, but
|
||||
Computer Use was stopped before any changed-binary native observation.
|
||||
- `context.json`: recorded source/toolchain, passing test-log and baseline-image
|
||||
hashes, and clean source after the test run.
|
||||
- `tests-01.log`: compilation failed before tests because `build/windows_manifest.rs`
|
||||
was excluded from the initial sparse checkout.
|
||||
- `tests-02.log`: compilation failed before tests because harness fixture files
|
||||
were excluded and a standalone test helper used `Self` instead of `Fixture`.
|
||||
The compiler was terminated after reporting errors; its interrupted exit is
|
||||
preserved. The missing tracked files and typo were repaired before run 03.
|
||||
|
||||
The tests exercise source-clearance proposals, OA/SA App event sequences,
|
||||
preview/release agreement, exact source/history, cancellation, and retained
|
||||
preview without commit authority on refused release. Existing collision and
|
||||
semantic checks still govern all proposals. Independent source review found the
|
||||
whole-header viewport restriction and obsolete preview assertion; both were
|
||||
corrected before the passing run. This is source/test review, not native review.
|
||||
|
||||
`baseline-left-refusal.png` is an unmodified actual native capture from the
|
||||
published preview executable, SHA256
|
||||
`2c03bd6cc3f1110c219e4f57ab94874a121590f83779239a93289b7170664399`.
|
||||
In the copied Allocation companion fixture, OA assign view at 1440x900 content
|
||||
and Fit 72%, dragging X from window (486,546) to (365,546) leaves allocation and
|
||||
placement unchanged and reports overlap. A control drag to (625,572) removes
|
||||
allocation, and Undo restores it. Intermediate jitter was diagnosed from source;
|
||||
this image is end-state evidence only.
|
||||
|
||||
Changed-binary native acceptance is still required: repeat the short fitted-view
|
||||
drag, identify and regrab the partially clipped card, check refused-motion
|
||||
feedback, Cancel and Undo/Redo, save the full bundle, close and reopen in a fresh
|
||||
process, and verify identity/allocation/placement and unchanged sibling data for
|
||||
OA and SA. Linux/WSL remains deferred by the user. No published package changed.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 129 KiB |
@@ -0,0 +1,5 @@
|
||||
Compiling iced_widget v0.14.2 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\vendor\iced_widget)
|
||||
Compiling iced v0.14.0
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-core)
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `dev` profile [unoptimized] target(s) in 5m 16s
|
||||
@@ -0,0 +1,6 @@
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-core)
|
||||
Checking iced_widget v0.14.2 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\vendor\iced_widget)
|
||||
Compiling oa-server v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-server)
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Checking iced v0.14.0
|
||||
Finished `dev` profile [unoptimized] target(s) in 4m 30s
|
||||
@@ -0,0 +1,2 @@
|
||||
Checking oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `dev` profile [unoptimized] target(s) in 1m 27s
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"source": "1a669130fb6d2b65279aab3847e966a1a282c191",
|
||||
"platform": "Microsoft Windows 10.0.26200",
|
||||
"compiler": [
|
||||
"rustc 1.98.1 (48a229cea 2026-09-01)",
|
||||
"binary: rustc",
|
||||
"commit-hash: 48a229ceaefd4985c50990b14116b6d856af0985",
|
||||
"commit-date: 2026-09-01",
|
||||
"host: x86_64-pc-windows-gnullvm",
|
||||
"release: 1.98.1",
|
||||
"LLVM version: 22.1.8"
|
||||
],
|
||||
"cargo": "cargo 1.98.1 (797e8a9bc 2026-08-05)",
|
||||
"test_log_sha256": "8EB57F522FA0DCCA327DC13C67A9C1B14F5FAB09C5599C108C0BA6D875C78886",
|
||||
"baseline_image_sha256": "AAF162619582018D1197D255B4BA1122747AA22703D8DEDFC7FCEFCB42AEFE38",
|
||||
"target": "x86_64-pc-windows-gnullvm",
|
||||
"incremental": "0",
|
||||
"jobs": "1",
|
||||
"working_tree_clean_after_test": true
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
# Independent source and evidence review
|
||||
|
||||
Fresh read-only GPT-6 Astra review, 2026-09-25. Production source
|
||||
`1a669130fb6d2b65279aab3847e966a1a282c191`; test-only successor
|
||||
`24b1eab07d306d8cb091bb46f7daf9f027a8f1c0`. No GUI actions or test execution
|
||||
were performed by this reviewer.
|
||||
|
||||
No material correctness finding in the bounded scope:
|
||||
|
||||
- The outgoing collision regression in `app/allocation_drag_tests.rs` proves a
|
||||
real source-boundary crossing and unobscured OutsidePerformers target, a
|
||||
nonempty clearance proposal, and intersection with unrelated work. Ordinary
|
||||
placement rejects that proposal. Release preserves project, source bundle,
|
||||
revision, snapshot identity and redo availability.
|
||||
- The retained-sibling regression first requires exact requested placement to
|
||||
fail. The admitted correction clears the enlarged source enclosure while
|
||||
preserving its projection, sibling allocation/placement and all other stored
|
||||
placements. Preview/release and source Undo/Redo comparisons are exact.
|
||||
- In `app/allocation_drag.rs`, clearance remains downstream of semantic
|
||||
admission and retries ordinary placement. Release recomputes placement and
|
||||
passes through `finish` and document execution. No collision, stale-state or
|
||||
retained-preview commit bypass was found.
|
||||
- The committed passing log's bytes and SHA256 match `context.json`; the
|
||||
baseline image hash also matches. Logs support 121 passing tests, strict
|
||||
Clippy and normal development build for the production source. They do not
|
||||
establish execution of the two successor tests; consult the linked raw logs
|
||||
and README for subsequent execution evidence.
|
||||
|
||||
The review supports the source policy and test design, not native usability.
|
||||
The 24x24 visible-header threshold alone does not prove readable identity or
|
||||
practical regrabbing. Changed-binary fitted-view dragging, feedback, recovery
|
||||
and Save/reopen remain unverified. This review is not the broad final
|
||||
requirements/domain/UX/release review required by `PROJECT_GOAL.md`.
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"source": "24b1eab07d306d8cb091bb46f7daf9f027a8f1c0",
|
||||
"production_source": "1a669130fb6d2b65279aab3847e966a1a282c191",
|
||||
"test_log": "tests-04.log",
|
||||
"test_log_sha256": "C98E0F70D60788B484146B3C8785891888BA696130ACC4BFD7D30DA570E6B1E1",
|
||||
"passed": 123,
|
||||
"failed": 0,
|
||||
"ignored": 0,
|
||||
"filtered": 1424,
|
||||
"native_candidate_sha256": "9BDA1A6A1D24FDC98C7A8822400C12FEA9AFE1DE828A82BA9273F03B99E47C07",
|
||||
"native_acceptance": "unverified"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-core)
|
||||
error: couldn't read `crates\oa-core\../../build/windows_manifest.rs`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-core\build.rs:1:1
|
||||
|
|
||||
1 | include!("../../build/windows_manifest.rs");
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: could not compile `oa-core` (build script) due to 1 previous error
|
||||
@@ -0,0 +1,68 @@
|
||||
Compiling oa-core v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-core)
|
||||
Compiling oa-server v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-server)
|
||||
Compiling iced_widget v0.14.2 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\vendor\iced_widget)
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Compiling iced v0.14.0
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/interface-operation-probe/output/boundary-only.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\app\reference_authoring\tests\mounted.rs:755:37
|
||||
|
|
||||
755 | let mut p = persistence::decode(include_bytes!(concat!(
|
||||
| _____________________________________^
|
||||
756 | | env!("CARGO_MANIFEST_DIR"),
|
||||
757 | | "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
758 | | )))
|
||||
| |______^
|
||||
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/interface-operation-probe/output/boundary-only.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\behavior\scenario\usage\tests.rs:235:37
|
||||
|
|
||||
235 | let mut p = persistence::decode(include_bytes!(concat!(
|
||||
| _____________________________________^
|
||||
236 | | env!("CARGO_MANIFEST_DIR"),
|
||||
237 | | "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
238 | | )))
|
||||
| |______^
|
||||
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/interface-operation-probe/output/boundary-only.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\behavior\scenario_operation_tests.rs:6:25
|
||||
|
|
||||
6 | persistence::decode(include_bytes!(concat!(
|
||||
| _________________________^
|
||||
7 | | env!("CARGO_MANIFEST_DIR"),
|
||||
8 | | "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
9 | | )))
|
||||
| |______^
|
||||
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/source7-native/base/expected-baseline.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\behavior\topology\contextual_routing.rs:61:29
|
||||
|
|
||||
61 | persistence::decode(include_bytes!(concat!(
|
||||
| _____________________________^
|
||||
62 | | env!("CARGO_MANIFEST_DIR"),
|
||||
63 | | "/../../harness/fixtures/source7-native/base/expected-baseline.json"
|
||||
64 | | )))
|
||||
| |__________^
|
||||
|
||||
error: couldn't read `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop/../../harness/fixtures/interface-operation-probe/output/boundary-only.json`: The system cannot find the path specified. (os error 3)
|
||||
--> crates\oa-desktop\src\semantic_review\source7_tests.rs:5:25
|
||||
|
|
||||
5 | persistence::decode(include_bytes!(concat!(
|
||||
| _________________________^
|
||||
6 | | env!("CARGO_MANIFEST_DIR"),
|
||||
7 | | "/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
||||
8 | | )))
|
||||
| |______^
|
||||
|
||||
error[E0411]: cannot find struct, variant or union type `Self` in this scope
|
||||
--> crates\oa-desktop\src\app\allocation_drag\clearance.rs:314:9
|
||||
|
|
||||
287 | fn fixture(layer: Layer) -> Fixture {
|
||||
| ------- `Self` not allowed in a function
|
||||
...
|
||||
314 | Self {
|
||||
| ^^^^ `Self` is only available in impls, traits, and type definitions
|
||||
|
||||
error: could not compile `oa-desktop` (bin "arcadia-studio" test) due to 6 previous errors
|
||||
|
||||
Caused by:
|
||||
process didn't exit successfully: `C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\rustup\toolchains\stable-x86_64-pc-windows-gnullvm\bin\rustc.exe --crate-name arcadia_studio --edition=2024 crates\oa-desktop\src\main.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --emit=dep-info,link -C embed-bitcode=no --test --check-cfg cfg(docsrs,test) --check-cfg "cfg(feature, values())" -C metadata=5e2096130f19ead2 -C extra-filename=-c5b42126ba36cd38 --out-dir C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps -C linker=clang.exe -C strip=debuginfo -L dependency=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps --extern axum=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libaxum-ccc7aea466db9258.rlib --extern iced=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libiced-594f7d417d69ed72.rlib --extern iced_runtime=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libiced_runtime-4d31a36fe5cef0b0.rlib --extern oa_core=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\liboa_core-2f3c5174cb6398fd.rlib --extern oa_server=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\liboa_server-bb42169ca297f969.rlib --extern reqwest=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libreqwest-31ac987472fa8038.rlib --extern rfd=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\librfd-17cc7ba2db223d7b.rlib --extern serde=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libserde-60a32311146a42e4.rlib --extern serde_json=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libserde_json-da2c4f333fc47818.rlib --extern tempfile=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libtempfile-86aac28d6be16d2f.rlib --extern tokio=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libtokio-5e0ca96735e3073a.rlib --extern unicode_segmentation=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\libunicode_segmentation-85698043f7780858.rlib -L native=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\build\ring-5f8d2d20440f09b4\out -L native=C:\Users\User\Documents\Git\ARCADIA-Studio\.local\toolchain\cargo\registry\src\index.crates.io-1949cf8c6b5b557f\windows_x86_64_gnullvm-0.52.6\lib -C link-arg=C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\build\oa-desktop-307d320bbc13ccd5\out\normal-user-manifest.o` (exit code: 0xffffffff)
|
||||
@@ -0,0 +1,130 @@
|
||||
Blocking waiting for file lock on build directory
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `test` profile [unoptimized] target(s) in 8m 13s
|
||||
Running unittests src\main.rs (C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\arcadia_studio-c5b42126ba36cd38.exe)
|
||||
|
||||
running 121 tests
|
||||
test allocation_drag::outgoing_header_retains_a_grab_region_without_requiring_full_camera_containment ... ok
|
||||
test allocation_drag::tests::allocation_artifact_and_manual_trace_cascades_are_named_and_refused ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_detects_hidden_moved_child_and_stationary_enclosure_growth ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_distributed_oa_sa_exact_scope_routes_source_history_and_anchor ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_noop_stale_tamper_target_and_all_selected_headers ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_performer_closure_does_not_acquire_incidental_semantic_root ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_existing_overlap_but_refuses_its_growth ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_roles_and_does_not_invent_unplaced_descendants ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_unrelated_folded_interior_diagnostic_boundary ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_simultaneous_roots_may_enter_each_others_vacated_bounds ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_validates_all_original_members_before_normalizing ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_checks_original_identity_and_visibility_before_normalization ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_distributed_parent_refuses_but_its_independent_leaves_can_reassign ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_finish_rejects_stale_project_and_each_tampered_result ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_normalizes_parent_child_and_assigns_unplaced_leaf_once ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_requires_non_anchor_selected_header_inside_viewport ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_unresolved_oa_role_leaf_accepts_explicit_target_but_not_outside ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_accumulates_stable_ids_and_aliases_without_partial_reuse ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_collateral_surviving_constraint_refuses_whole_semantic_candidate ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_deletes_only_own_routes_across_views_and_keeps_internal_flow ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_independent_performers_preserves_already_correct_provenance ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_source_failure_preserves_existing_history_and_retry_ids ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_unfolds_only_changed_destination_and_refuses_newly_exposed_overlap ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_uses_vacated_space_and_one_complete_route_owner_map ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_zero_delta_changes_semantics_once_and_then_is_noop ... ok
|
||||
test allocation_drag::tests::cache_plan_uses_candidate_index_and_same_pixel_different_target_changes_preview ... ok
|
||||
test allocation_drag::tests::collapsed_target_unfold_is_explicit_and_no_other_fold_changes ... ok
|
||||
test allocation_drag::tests::direct_exit_revealing_another_role_performer_has_truthful_refusal ... ok
|
||||
test allocation_drag::tests::explicit_different_performer_retains_advisory_role_warning ... ok
|
||||
test allocation_drag::tests::folded_subtree_cannot_hide_new_collision ... ok
|
||||
test allocation_drag::tests::geometry_refuses_new_overlap_bounds_and_invisible_headers_and_preserves_existing_overlap ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_moved_folded_child_collision ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_stationary_enclosure_growth_collision ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_distributed_root_and_performer_can_translate_rigidly ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_unrelated_folded_interiors_are_outside_diagnostic_domain ... ok
|
||||
test allocation_drag::tests::malformed_scope_wrong_projection_duplicate_placement_and_stable_id_collision_refuse ... ok
|
||||
test allocation_drag::tests::mixed_p_x_y_refuses_semantic_and_arrange_instead_of_stretching_destination ... ok
|
||||
test allocation_drag::tests::oa_communication_incompleteness_remains_advisory_without_collateral_loss ... ok
|
||||
test allocation_drag::tests::oa_sa_incoming_between_outgoing_preserve_identity_source_history_and_other_views ... ok
|
||||
test allocation_drag::tests::oa_sa_one_leaf_move_refuses_when_ancestor_regrouping_introduces_unrelated_overlap ... ok
|
||||
test allocation_drag::tests::oa_sa_one_leaf_reassignment_and_outward_move_regroup_ancestors_without_moving_sibling ... ok
|
||||
test allocation_drag::tests::oa_sa_populated_collapsed_target_accepts_room_and_refuses_collision_without_relocating_contents ... ok
|
||||
test allocation_drag::tests::oa_sa_state_owned_action_and_availability_reject_reassignment_atomically ... ok
|
||||
test allocation_drag::tests::oa_sa_unfolding_prior_hidden_overlap_refuses_even_when_insertion_does_not_collide ... ok
|
||||
test allocation_drag::tests::own_deleted_allocation_routes_may_disappear_in_every_diagram_only ... ok
|
||||
test allocation_drag::tests::role_only_same_effective_performer_is_semantic_noop_and_outgoing_refuses ... ok
|
||||
test allocation_drag::tests::sa_port_and_deferred_exchange_allocation_cascade_refuses_and_retarget_is_invalid ... ok
|
||||
test allocation_drag::tests::same_performer_readability_preserves_snapped_noop_and_allocation_identity ... ok
|
||||
test allocation_drag::tests::scope_detects_first_child_allocation_and_role_transfer_and_finish_rejects_live_edits ... ok
|
||||
test allocation_drag::tests::selected_header_visibility_allows_viewport_edge_but_refuses_clipping ... ok
|
||||
test allocation_drag::tests::snapped_noop_returns_no_command_and_keeps_source_revision_and_redo ... ok
|
||||
test allocation_drag::tests::source_budget_refusal_keeps_project_bundle_history_and_retry_identity_exact ... ok
|
||||
test allocation_drag::tests::surviving_constraint_mutation_is_refused_even_when_core_candidate_is_valid ... ok
|
||||
test allocation_drag::tests::target_presentation_requires_accepted_identity_and_selected_header_visibility ... ok
|
||||
test allocation_drag::tests::toolbar_identity_allows_exact_incoming_placement_at_old_label_overlap ... ok
|
||||
test allocation_drag::tests::uniform_folded_grandchild_and_unplaced_scope_moves_exactly_q_and_all_l_oa_sa ... ok
|
||||
test allocation_drag::tests::uniform_role_only_and_mixed_direct_role_composites_same_target_do_not_allocate ... ok
|
||||
test allocation_drag::tests::uniformly_unassigned_composite_creates_only_stable_leaf_allocations_including_unplaced ... ok
|
||||
test allocation_drag::tests::unrequested_view_option_cleanup_is_not_whitelisted ... ok
|
||||
test allocation_drag::tests::unresolved_role_leaf_can_arrange_or_accept_explicit_target_without_guessing ... ok
|
||||
test app::allocation_drag::clearance::tests::corner_order_follows_boundary_crossing_and_is_deterministic ... ok
|
||||
test app::allocation_drag::clearance::tests::descendant_footprint_controls_clearance_without_mutating_projects ... ok
|
||||
test app::allocation_drag::clearance::tests::each_outgoing_edge_clears_the_source_and_passes_ordinary_placement ... ok
|
||||
test app::allocation_drag::clearance::tests::independent_selected_roots_keep_one_rigid_group_translation ... ok
|
||||
test app::allocation_drag::clearance::tests::no_crossing_no_overlap_no_source_and_nonfinite_requests_are_empty ... ok
|
||||
test app::allocation_drag::clearance::tests::source_shrink_uses_candidate_enclosure_not_frozen_target ... ok
|
||||
test app::allocation_drag::tests::actual_canvas_origin_change_cancels_held_work_before_any_legacy_release ... ok
|
||||
test app::allocation_drag::tests::arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source ... ok
|
||||
test app::allocation_drag::tests::candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index ... ok
|
||||
test app::allocation_drag::tests::destination_identity_does_not_describe_blocked_or_outside_viewport_as_unallocation ... ok
|
||||
test app::allocation_drag::tests::frozen_collapsed_target_uses_only_visible_header_and_port_edge_controls_occlude ... ok
|
||||
test app::allocation_drag::tests::frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overlap ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_description_retains_focus_and_middle_caret_for_next_key ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_name_retains_focus_and_middle_caret_for_next_key ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_exact_incoming_drag_keeps_destination_visible_and_canvas_stable ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_keyboard_arrange_shortcut_consumes_once_escape_cancels_and_dirty_owner_refuses ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_rendered_canvas_pixels_do_not_change_when_toolbar_feedback_text_changes ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_toolbar_hover_inspects_last_full_name_without_authorizing_an_outside_drop ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_tree_outgoing_boundary_motion_preview_release_and_cancel ... ok
|
||||
test app::allocation_drag::tests::full_tree::control_focus::dirty_selected_pin_outer_ring_keeps_real_name_focus_and_caret ... ok
|
||||
test app::allocation_drag::tests::group::allocation_bare_moves_cannot_bypass_armed_active_or_retired_lifetime ... ok
|
||||
test app::allocation_drag::tests::group::completed_text_apply_rejects_old_allocation_packets_without_retiring_a_new_drag ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_after_cancel_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_context_mismatches_preserve_the_current_session ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_current_restart_and_pair_commit_after_tool_boundary ... ok
|
||||
test app::allocation_drag::tests::group::group_arrange_work_and_performer_anchors_preserve_every_responsibility ... ok
|
||||
test app::allocation_drag::tests::group::group_assignment_split_rebuild_preserves_primary_and_commits_one_complete_candidate ... ok
|
||||
test app::allocation_drag::tests::group::group_out_and_back_assigns_at_zero_delta_then_true_noop_preserves_redo ... ok
|
||||
test app::allocation_drag::tests::group::group_selection_aba_and_old_press_cannot_retire_a_new_token ... ok
|
||||
test app::allocation_drag::tests::group::group_source_refusal_retains_primary_source_and_both_history_paths ... ok
|
||||
test app::allocation_drag::tests::group::idle_group_press_rejects_selection_generation_aba_with_unchanged_other_stamps ... ok
|
||||
test app::allocation_drag::tests::group::ordinary_off_grid_performer_out_and_back_is_noop_with_redo ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::captured_route_context_fields_independently_reject_before_retirement ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::current_route_commits_without_session_preserve_exact_route_history ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::same_context_route_attachment_after_selection_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::same_context_route_commit_after_selection_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::stale_route_attachment_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::stale_route_commit_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::unsupported_group_click_collapses_but_threshold_crossing_never_becomes_click ... ok
|
||||
test app::allocation_drag::tests::modal_roundtrip_equal_fit_undo_resize_and_focus_retire_press_before_replay ... ok
|
||||
test app::allocation_drag::tests::mounted_clean_stationary_selects_exact_work_without_revision_or_legacy_apply ... ok
|
||||
test app::allocation_drag::tests::mounted_dirty_port_press_cannot_fall_through_to_applying_port_selection ... ok
|
||||
test app::allocation_drag::tests::mounted_dirty_property_press_does_not_select_apply_or_destroy_draft ... ok
|
||||
test app::allocation_drag::tests::mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_steps ... ok
|
||||
test app::allocation_drag::tests::mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creates_no_allocation ... ok
|
||||
test app::allocation_drag::tests::mounted_uniform_parent_moves_hidden_placements_and_assigns_only_leaves ... ok
|
||||
test app::allocation_drag::tests::navigation_union::old_chooser_envelopes_preserve_a_current_canvas_gesture ... ok
|
||||
test app::allocation_drag::tests::navigation_union::stale_gesture_packets_preserve_navigation_and_a_new_live_cancel_still_works ... ok
|
||||
test app::allocation_drag::tests::oa_sa_clipped_selected_header_refuses_preview_release_and_retries_without_partial_source_or_history ... ok
|
||||
test app::allocation_drag::tests::oa_sa_toolbar_identity_accepts_exact_receiver_and_collapsed_header_drag ... ok
|
||||
test app::allocation_drag::tests::outgoing_boundary_drop_clears_source_without_changing_other_placements ... ok
|
||||
test app::allocation_drag::tests::outside_release_is_cancel_and_does_not_remove_an_assignment ... ok
|
||||
test app::allocation_drag::tests::pending_behavior_draft_refuses_actual_work_press_and_equal_document_replacement_retires_packet ... ok
|
||||
test app::allocation_drag::tests::refused_motion_retains_last_scene_but_release_never_commits_it ... ok
|
||||
test app::allocation_drag::tests::release_uses_ordered_native_position_before_later_motion_in_same_batch ... ok
|
||||
test app::allocation_drag::tests::same_snapped_position_on_opposite_shared_border_rebuilds_semantics_even_if_layout_refuses ... ok
|
||||
test app::allocation_drag::tests::source_and_modal_admission_reject_prebuilt_clean_press_without_selecting ... ok
|
||||
test app::allocation_drag::tests::source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision ... ok
|
||||
test app::allocation_drag::tests::stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once ... ok
|
||||
test app::allocation_drag::tests::toolbar_destination_names_transformed_target_whose_header_is_clipped ... ok
|
||||
test app::allocation_drag::tests::toolbar_status_leaves_transformed_top_edge_work_header_readable ... ok
|
||||
|
||||
test result: ok. 121 passed; 0 failed; 0 ignored; 0 measured; 1424 filtered out; finished in 35.33s
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `test` profile [unoptimized] target(s) in 6m 05s
|
||||
Running unittests src\main.rs (C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\arcadia_studio-c5b42126ba36cd38.exe)
|
||||
|
||||
running 123 tests
|
||||
test allocation_drag::outgoing_header_retains_a_grab_region_without_requiring_full_camera_containment ... ok
|
||||
test allocation_drag::tests::allocation_artifact_and_manual_trace_cascades_are_named_and_refused ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_detects_hidden_moved_child_and_stationary_enclosure_growth ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_distributed_oa_sa_exact_scope_routes_source_history_and_anchor ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_noop_stale_tamper_target_and_all_selected_headers ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_performer_closure_does_not_acquire_incidental_semantic_root ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_existing_overlap_but_refuses_its_growth ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_roles_and_does_not_invent_unplaced_descendants ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_preserves_unrelated_folded_interior_diagnostic_boundary ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_simultaneous_roots_may_enter_each_others_vacated_bounds ... ok
|
||||
test allocation_drag::tests::arrange_tests::explicit_arrange_validates_all_original_members_before_normalizing ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_checks_original_identity_and_visibility_before_normalization ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_distributed_parent_refuses_but_its_independent_leaves_can_reassign ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_finish_rejects_stale_project_and_each_tampered_result ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_normalizes_parent_child_and_assigns_unplaced_leaf_once ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_requires_non_anchor_selected_header_inside_viewport ... ok
|
||||
test allocation_drag::tests::assignment_boundary_tests::group_assignment_unresolved_oa_role_leaf_accepts_explicit_target_but_not_outside ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_accumulates_stable_ids_and_aliases_without_partial_reuse ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_collateral_surviving_constraint_refuses_whole_semantic_candidate ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_deletes_only_own_routes_across_views_and_keeps_internal_flow ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_independent_performers_preserves_already_correct_provenance ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_source_failure_preserves_existing_history_and_retry_ids ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_unfolds_only_changed_destination_and_refuses_newly_exposed_overlap ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_uses_vacated_space_and_one_complete_route_owner_map ... ok
|
||||
test allocation_drag::tests::assignment_tests::group_assignment_zero_delta_changes_semantics_once_and_then_is_noop ... ok
|
||||
test allocation_drag::tests::cache_plan_uses_candidate_index_and_same_pixel_different_target_changes_preview ... ok
|
||||
test allocation_drag::tests::collapsed_target_unfold_is_explicit_and_no_other_fold_changes ... ok
|
||||
test allocation_drag::tests::direct_exit_revealing_another_role_performer_has_truthful_refusal ... ok
|
||||
test allocation_drag::tests::explicit_different_performer_retains_advisory_role_warning ... ok
|
||||
test allocation_drag::tests::folded_subtree_cannot_hide_new_collision ... ok
|
||||
test allocation_drag::tests::geometry_refuses_new_overlap_bounds_and_invisible_headers_and_preserves_existing_overlap ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_moved_folded_child_collision ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_detects_stationary_enclosure_growth_collision ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_distributed_root_and_performer_can_translate_rigidly ... ok
|
||||
test allocation_drag::tests::group_arrange_experiment::mixed_arrange_experiment_unrelated_folded_interiors_are_outside_diagnostic_domain ... ok
|
||||
test allocation_drag::tests::malformed_scope_wrong_projection_duplicate_placement_and_stable_id_collision_refuse ... ok
|
||||
test allocation_drag::tests::mixed_p_x_y_refuses_semantic_and_arrange_instead_of_stretching_destination ... ok
|
||||
test allocation_drag::tests::oa_communication_incompleteness_remains_advisory_without_collateral_loss ... ok
|
||||
test allocation_drag::tests::oa_sa_incoming_between_outgoing_preserve_identity_source_history_and_other_views ... ok
|
||||
test allocation_drag::tests::oa_sa_one_leaf_move_refuses_when_ancestor_regrouping_introduces_unrelated_overlap ... ok
|
||||
test allocation_drag::tests::oa_sa_one_leaf_reassignment_and_outward_move_regroup_ancestors_without_moving_sibling ... ok
|
||||
test allocation_drag::tests::oa_sa_populated_collapsed_target_accepts_room_and_refuses_collision_without_relocating_contents ... ok
|
||||
test allocation_drag::tests::oa_sa_state_owned_action_and_availability_reject_reassignment_atomically ... ok
|
||||
test allocation_drag::tests::oa_sa_unfolding_prior_hidden_overlap_refuses_even_when_insertion_does_not_collide ... ok
|
||||
test allocation_drag::tests::own_deleted_allocation_routes_may_disappear_in_every_diagram_only ... ok
|
||||
test allocation_drag::tests::role_only_same_effective_performer_is_semantic_noop_and_outgoing_refuses ... ok
|
||||
test allocation_drag::tests::sa_port_and_deferred_exchange_allocation_cascade_refuses_and_retarget_is_invalid ... ok
|
||||
test allocation_drag::tests::same_performer_readability_preserves_snapped_noop_and_allocation_identity ... ok
|
||||
test allocation_drag::tests::scope_detects_first_child_allocation_and_role_transfer_and_finish_rejects_live_edits ... ok
|
||||
test allocation_drag::tests::selected_header_visibility_allows_viewport_edge_but_refuses_clipping ... ok
|
||||
test allocation_drag::tests::snapped_noop_returns_no_command_and_keeps_source_revision_and_redo ... ok
|
||||
test allocation_drag::tests::source_budget_refusal_keeps_project_bundle_history_and_retry_identity_exact ... ok
|
||||
test allocation_drag::tests::surviving_constraint_mutation_is_refused_even_when_core_candidate_is_valid ... ok
|
||||
test allocation_drag::tests::target_presentation_requires_accepted_identity_and_selected_header_visibility ... ok
|
||||
test allocation_drag::tests::toolbar_identity_allows_exact_incoming_placement_at_old_label_overlap ... ok
|
||||
test allocation_drag::tests::uniform_folded_grandchild_and_unplaced_scope_moves_exactly_q_and_all_l_oa_sa ... ok
|
||||
test allocation_drag::tests::uniform_role_only_and_mixed_direct_role_composites_same_target_do_not_allocate ... ok
|
||||
test allocation_drag::tests::uniformly_unassigned_composite_creates_only_stable_leaf_allocations_including_unplaced ... ok
|
||||
test allocation_drag::tests::unrequested_view_option_cleanup_is_not_whitelisted ... ok
|
||||
test allocation_drag::tests::unresolved_role_leaf_can_arrange_or_accept_explicit_target_without_guessing ... ok
|
||||
test app::allocation_drag::clearance::tests::corner_order_follows_boundary_crossing_and_is_deterministic ... ok
|
||||
test app::allocation_drag::clearance::tests::descendant_footprint_controls_clearance_without_mutating_projects ... ok
|
||||
test app::allocation_drag::clearance::tests::each_outgoing_edge_clears_the_source_and_passes_ordinary_placement ... ok
|
||||
test app::allocation_drag::clearance::tests::independent_selected_roots_keep_one_rigid_group_translation ... ok
|
||||
test app::allocation_drag::clearance::tests::no_crossing_no_overlap_no_source_and_nonfinite_requests_are_empty ... ok
|
||||
test app::allocation_drag::clearance::tests::source_shrink_uses_candidate_enclosure_not_frozen_target ... ok
|
||||
test app::allocation_drag::tests::actual_canvas_origin_change_cancels_held_work_before_any_legacy_release ... ok
|
||||
test app::allocation_drag::tests::arrange_is_one_gesture_and_snapped_noop_preserves_redo_and_source ... ok
|
||||
test app::allocation_drag::tests::candidate_semantics_are_cached_per_destination_with_stable_ids_and_own_index ... ok
|
||||
test app::allocation_drag::tests::destination_identity_does_not_describe_blocked_or_outside_viewport_as_unallocation ... ok
|
||||
test app::allocation_drag::tests::frozen_collapsed_target_uses_only_visible_header_and_port_edge_controls_occlude ... ok
|
||||
test app::allocation_drag::tests::frozen_targets_use_half_open_semantic_priority_and_refuse_incomparable_overlap ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_description_retains_focus_and_middle_caret_for_next_key ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_dirty_name_retains_focus_and_middle_caret_for_next_key ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_app_tree_exact_incoming_drag_keeps_destination_visible_and_canvas_stable ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_keyboard_arrange_shortcut_consumes_once_escape_cancels_and_dirty_owner_refuses ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_rendered_canvas_pixels_do_not_change_when_toolbar_feedback_text_changes ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_toolbar_hover_inspects_last_full_name_without_authorizing_an_outside_drop ... ok
|
||||
test app::allocation_drag::tests::full_tree::actual_tree_outgoing_boundary_motion_preview_release_and_cancel ... ok
|
||||
test app::allocation_drag::tests::full_tree::control_focus::dirty_selected_pin_outer_ring_keeps_real_name_focus_and_caret ... ok
|
||||
test app::allocation_drag::tests::group::allocation_bare_moves_cannot_bypass_armed_active_or_retired_lifetime ... ok
|
||||
test app::allocation_drag::tests::group::completed_text_apply_rejects_old_allocation_packets_without_retiring_a_new_drag ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_after_cancel_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_context_mismatches_preserve_the_current_session ... ok
|
||||
test app::allocation_drag::tests::group::connection_lifetime::captured_connection_current_restart_and_pair_commit_after_tool_boundary ... ok
|
||||
test app::allocation_drag::tests::group::group_arrange_work_and_performer_anchors_preserve_every_responsibility ... ok
|
||||
test app::allocation_drag::tests::group::group_assignment_split_rebuild_preserves_primary_and_commits_one_complete_candidate ... ok
|
||||
test app::allocation_drag::tests::group::group_out_and_back_assigns_at_zero_delta_then_true_noop_preserves_redo ... ok
|
||||
test app::allocation_drag::tests::group::group_selection_aba_and_old_press_cannot_retire_a_new_token ... ok
|
||||
test app::allocation_drag::tests::group::group_source_refusal_retains_primary_source_and_both_history_paths ... ok
|
||||
test app::allocation_drag::tests::group::idle_group_press_rejects_selection_generation_aba_with_unchanged_other_stamps ... ok
|
||||
test app::allocation_drag::tests::group::ordinary_off_grid_performer_out_and_back_is_noop_with_redo ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::captured_route_context_fields_independently_reject_before_retirement ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::current_route_commits_without_session_preserve_exact_route_history ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::same_context_route_attachment_after_selection_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::same_context_route_commit_after_selection_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::stale_route_attachment_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::route_lifetime::stale_route_commit_preserves_arrange_arm_and_live_group ... ok
|
||||
test app::allocation_drag::tests::group::unsupported_group_click_collapses_but_threshold_crossing_never_becomes_click ... ok
|
||||
test app::allocation_drag::tests::modal_roundtrip_equal_fit_undo_resize_and_focus_retire_press_before_replay ... ok
|
||||
test app::allocation_drag::tests::mounted_clean_stationary_selects_exact_work_without_revision_or_legacy_apply ... ok
|
||||
test app::allocation_drag::tests::mounted_dirty_port_press_cannot_fall_through_to_applying_port_selection ... ok
|
||||
test app::allocation_drag::tests::mounted_dirty_property_press_does_not_select_apply_or_destroy_draft ... ok
|
||||
test app::allocation_drag::tests::mounted_oa_sa_ordered_incoming_between_outgoing_are_exact_single_history_steps ... ok
|
||||
test app::allocation_drag::tests::mounted_role_only_outgoing_refusal_retains_placement_and_same_performer_creates_no_allocation ... ok
|
||||
test app::allocation_drag::tests::mounted_uniform_parent_moves_hidden_placements_and_assigns_only_leaves ... ok
|
||||
test app::allocation_drag::tests::navigation_union::old_chooser_envelopes_preserve_a_current_canvas_gesture ... ok
|
||||
test app::allocation_drag::tests::navigation_union::stale_gesture_packets_preserve_navigation_and_a_new_live_cancel_still_works ... ok
|
||||
test app::allocation_drag::tests::oa_sa_clipped_selected_header_refuses_preview_release_and_retries_without_partial_source_or_history ... ok
|
||||
test app::allocation_drag::tests::oa_sa_toolbar_identity_accepts_exact_receiver_and_collapsed_header_drag ... ok
|
||||
test app::allocation_drag::tests::outgoing_boundary_drop_clears_source_without_changing_other_placements ... ok
|
||||
test app::allocation_drag::tests::outgoing_clearance_collision_refuses_release_without_source_or_history_mutation ... ok
|
||||
test app::allocation_drag::tests::outgoing_clearance_preserves_source_enclosure_and_allocated_sibling ... ok
|
||||
test app::allocation_drag::tests::outside_release_is_cancel_and_does_not_remove_an_assignment ... ok
|
||||
test app::allocation_drag::tests::pending_behavior_draft_refuses_actual_work_press_and_equal_document_replacement_retires_packet ... ok
|
||||
test app::allocation_drag::tests::refused_motion_retains_last_scene_but_release_never_commits_it ... ok
|
||||
test app::allocation_drag::tests::release_uses_ordered_native_position_before_later_motion_in_same_batch ... ok
|
||||
test app::allocation_drag::tests::same_snapped_position_on_opposite_shared_border_rebuilds_semantics_even_if_layout_refuses ... ok
|
||||
test app::allocation_drag::tests::source_and_modal_admission_reject_prebuilt_clean_press_without_selecting ... ok
|
||||
test app::allocation_drag::tests::source_refusal_is_atomic_and_a_new_gesture_can_retry_against_live_revision ... ok
|
||||
test app::allocation_drag::tests::stale_packets_do_not_cancel_a_newer_token_and_each_release_is_consumed_once ... ok
|
||||
test app::allocation_drag::tests::toolbar_destination_names_transformed_target_whose_header_is_clipped ... ok
|
||||
test app::allocation_drag::tests::toolbar_status_leaves_transformed_top_edge_work_header_readable ... ok
|
||||
|
||||
test result: ok. 123 passed; 0 failed; 0 ignored; 0 measured; 1424 filtered out; finished in 54.86s
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
*.log -text
|
||||
*.rs -text
|
||||
@@ -0,0 +1,153 @@
|
||||
# Cold construction and pan alternatives
|
||||
|
||||
Follow-up to [the stationary-camera cache](README.md), for issue 44. Application
|
||||
source remains `336720559bcec511949caa8a42bee8a55c3822da`, with evidence checkpoint
|
||||
`2ca84676ee4dc30846208881a27d90a901482085`. These standalone experiments change no
|
||||
application code, dependencies, event handling or release. Native interaction
|
||||
remains paused; no result below establishes native FPS.
|
||||
|
||||
## Question and method
|
||||
|
||||
The existing cache removes repeated construction at identical camera inputs but
|
||||
misses during pan/zoom. Can simpler geometry or renderer-level translation remove
|
||||
that cost without changing the grid's appearance? Use the same unoptimized Iced
|
||||
0.14 WGPU dependencies and Windows environment recorded in `context.json`.
|
||||
Compile the new drivers with the same `rustc`/extern/linker arguments described
|
||||
in README, replacing the driver filename and output executable. No desktop window
|
||||
is created and no input is sent. Run `cold_grid_probe` with argument `20`; phase
|
||||
drivers take no arguments.
|
||||
|
||||
The cold driver compares original circles, one reusable origin circle transformed
|
||||
for each fill, one path per column, and direct 1.15-unit squares. Every candidate
|
||||
also constructs one origin-circle template; this small common overhead is retained.
|
||||
Three warmups precede 20 changing-pan samples. Construction ends before geometry
|
||||
destruction, rendering and readback. Percentiles use sorted index `ceil((n-1)*p)`.
|
||||
Fixed candidate ordering and uncontrolled OS load limit small timing comparisons.
|
||||
|
||||
## Cold construction results
|
||||
|
||||
`cold_grid_probe.rs` produces `grid-cold-01.log`. Median milliseconds:
|
||||
|
||||
| Canvas, zoom | Original | Reused circle | Column batch | Square |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| 1900x660, 1.0 | 61.0564 | 55.3975 | 54.3594 | 3.5527 |
|
||||
| 1900x660, 0.5 | 215.8919 | 162.3073 | 158.5968 | 7.5699 |
|
||||
| 1440x900, 1.0 | 60.1683 | 43.3860 | 60.8198 | 2.0340 |
|
||||
| 1440x900, 0.5 | 165.2049 | 133.3420 | 143.1489 | 6.8178 |
|
||||
|
||||
Both circle alternatives exactly match original pixels in all 12 sampled cases
|
||||
(four integer canvas/zoom pairs, three pan offsets, scale 1). This does not prove
|
||||
equivalence at fractional zoom, other DPI or other camera inputs. Squares match
|
||||
eight of those cases but differ at the third pan offset in each canvas/zoom pair:
|
||||
4,266–13,184 changed pixels, maximum channel difference 7. Their different shape
|
||||
and area are an explicit appearance change, not a pixel-preserving optimization.
|
||||
|
||||
Reusing circles and column batches leave substantial cold cost; the experiment
|
||||
does not justify adding them to production as the solution to low FPS. The square
|
||||
alternative is much faster in this scope, but is not adopted without evaluating
|
||||
its appearance and the actual interaction result. The previous all-grid single
|
||||
path experiment remains rejected for its much higher construction cost.
|
||||
|
||||
## Source explanation and independent review
|
||||
|
||||
A fresh read-only critic checked the probes and actual local Iced source. Each
|
||||
`Path::circle` allocates/builds a path and converts its arc into quadratic segments.
|
||||
Each WGPU `Frame::fill` allocates a boxed geometry builder and invokes Lyon fill
|
||||
tessellation. Same-color fills already share a solid mesh buffer: this is not
|
||||
one GPU draw call per dot. Frame translation transforms paths before tessellation,
|
||||
so path reuse alone does not reuse triangles.
|
||||
|
||||
Renderer-level `with_translation` can reuse cached triangles. Stock Canvas cannot
|
||||
attach a separate transform to each returned geometry; application adoption would
|
||||
need a small background widget or wrapper, with layout, clipping, lifecycle and
|
||||
interaction checks. Scaling the grid to handle zoom is unsuitable because the
|
||||
dot radius must remain fixed in screen units.
|
||||
|
||||
The critic also identified two important controls. Original opaque background
|
||||
geometry must not silently become screenshot-clear color: MSAA/alpha composition
|
||||
can change quantized pixels. Also, `iced_wgpu::triangle::Storage::prepare` rewrites
|
||||
cached mesh data when its transform changes. Construction/command-recording time
|
||||
therefore excludes real preparation/upload costs. The later probe measures a
|
||||
rendering/readback path separately; neither timer represents native presentation.
|
||||
|
||||
Primary sources: Iced 0.14 `iced_graphics/src/geometry/path{,/builder}.rs`,
|
||||
`iced_wgpu/src/{geometry,triangle}.rs`, `iced_core/src/renderer.rs`, and
|
||||
`iced_widget/src/canvas.rs`. The reviewer changed no files and did not execute the
|
||||
probes; execution and interpretation remain the parent's responsibility.
|
||||
|
||||
## Renderer-translated phase cache
|
||||
|
||||
`phase_grid_probe_v1.rs` / `grid-phase-01.log` cache zero-phase circles once for
|
||||
each size/spacing pair, translate by `pan.rem_euclid(step)`, and externally clip
|
||||
to the original viewport. The original nonnegative row/column indices and fixed
|
||||
screen-space radius are retained. Cache clip bounds include the radius margins.
|
||||
This first prototype uses screenshot clear color instead of opaque background
|
||||
geometry; its widespread one-level pixel differences are not accepted as parity.
|
||||
Its compiler emitted one duplicate Renderer-import warning, retained in the
|
||||
original driver. Later drivers remove that import and compile with `-D warnings`.
|
||||
|
||||
`phase_grid_opaque_probe.rs` / `grid-phase-opaque-01.log` add an oversized opaque
|
||||
background to the same cached geometry as the circles. Its bounds extend from
|
||||
`(-step,-step)` to `(width+2*step,height+2*step)`, covering the viewport at every
|
||||
positive remainder without adding extra dot rows or columns. This directly
|
||||
compares against the unchanged legacy painter. A proposed separate-background
|
||||
control was not executed; no result is claimed for it.
|
||||
|
||||
Of 75 comparisons (five size/zoom pairs, five offsets, three DPI scales), 73 are
|
||||
byte-exact. All 60 cases at integer canvas sizes and spacing 14 or 24 are exact.
|
||||
At 301.5x211.25, zoom 0.73, pan (-13.25,9.75), scale 1 changes 12 pixels by at
|
||||
most one channel level; scale 2 changes one pixel by at most six levels. These
|
||||
are failures of exact parity and are preserved, not hidden behind a tolerance.
|
||||
|
||||
The targeted `phase_grid_diagnostic.rs` / `grid-phase-diagnostic-01.log` locates
|
||||
all 12 scale-1 differences at the rightmost physical column x=301. The scale-2
|
||||
single-pixel difference does not recur. That diagnostic skips earlier large
|
||||
cases and their rendering/timing sequence: it does not exonerate the earlier
|
||||
failure or establish that every residual difference is at an edge.
|
||||
|
||||
The replay driver's first compilation failed with E0689 because the new control's
|
||||
`Vector::new(-13.25,9.75)` left its numeric type ambiguous before `rem_euclid`.
|
||||
An explicit `Vector<f32>` annotation corrects the experiment harness. No probe
|
||||
executed during that failed attempt; no application source changed.
|
||||
|
||||
`phase_grid_replay.rs` preserves the full opaque probe's rendering/timing sequence
|
||||
and adds coordinate diagnostics without extra renders until that sequence ends.
|
||||
`grid-phase-replay-01.log` reproduces both failures: the 12 right-edge pixels and
|
||||
one **interior** pixel at physical (149,263), where legacy RGBA is
|
||||
`[240,243,246,255]` and cached RGBA is `[246,248,251,255]`. The latter cannot be
|
||||
dismissed as viewport-edge rounding. Afterwards, all 12 same-renderer repeat
|
||||
controls (three repetitions of old/old and cached/cached at two scales) are
|
||||
byte-exact; the control cache builds once. This finite control does not establish
|
||||
universal determinism, but supports investigating differing geometry coverage
|
||||
instead of treating the reproduced pixel as random readback noise.
|
||||
|
||||
Cold construction plus command recording takes 68.42–173.72ms median for the four
|
||||
large viewports; cached translation takes 0.0008–0.0011ms. Each per-size/spacing
|
||||
cache builds exactly once through all pan comparisons and measurements. Zoom
|
||||
and resize through a single retained cache are not tested by these drivers.
|
||||
The labels containing `submit` mean renderer command recording, not GPU submission.
|
||||
|
||||
Two selected cases include construction, recording, backend preparation and GPU
|
||||
readback in a separate timer (two warmups, eight samples, sorted-index median):
|
||||
|
||||
| Canvas, zoom | Original p50 ms | Phase cache p50 ms |
|
||||
| --- | ---: | ---: |
|
||||
| 1900x660, 1.0 | 259.9265 | 212.6536 |
|
||||
| 1440x900, 0.5 | 337.2560 | 170.3780 |
|
||||
|
||||
These support a benefit within the headless path, but retain significant renderer
|
||||
and readback cost and variable outliers. They are not native frame times. Sample
|
||||
size, sequential ordering and uncontrolled machine load prohibit a precise
|
||||
general speedup claim. All driver/log hashes and execution mapping are in
|
||||
`cold-pan-context.json`.
|
||||
|
||||
The phase candidate removes the measured pan tessellation cost, but does not
|
||||
meet exact visual preservation. Do not adopt it as an invisible optimization.
|
||||
The small circle rewrites leave the main cost and squares change appearance.
|
||||
Further work should investigate the reproduced interior coverage difference or
|
||||
compare optimized dependency/build profiles before adding a custom widget.
|
||||
Any adopted rendering candidate must then verify a real mounted scene at a
|
||||
nonzero widget origin, foreground ordering, viewport clipping, pan/zoom/resize
|
||||
cache lifecycle and unchanged event behavior. Native interaction/FPS verification
|
||||
remains a separate required gate. No application implementation is changed by
|
||||
these experiments.
|
||||
@@ -0,0 +1,180 @@
|
||||
# Architecture background geometry cache
|
||||
|
||||
Scope: [issue 44](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/issues/44).
|
||||
Production/test candidate: `636093aae67aa48fa1d35e403a205227e81d04de`, based on
|
||||
`c0908f101ac376afb8021e60e454e2a176e49d45`. The base includes the separately
|
||||
unaccepted outgoing-drag contribution for issue 40. This is a contribution
|
||||
experiment and repair, not an integrated release or full FPS acceptance.
|
||||
|
||||
## Question and evidence
|
||||
|
||||
The [preceding core probe](../architecture-perf-20260925/README.md) measured
|
||||
approximately 4.26ms median for preparation/projection/routing in an eight-card
|
||||
approximation. It did not reproduce the reported severe slowdown. Source review
|
||||
then identified repeated grid construction on every canvas redraw, despite the
|
||||
grid's independence from model edits, selection and pointer position.
|
||||
|
||||
The standalone headless Iced probe copies the former grid loop: background fill,
|
||||
then one radius-0.65 circle per grid point, with the same spacing, pan remainder
|
||||
and colors. It measures geometry construction independently from offscreen
|
||||
rendering/readback, using the actual compiled development-profile Iced renderer.
|
||||
It creates no desktop window and sends no input. Three warmups precede 20 samples
|
||||
per phase. The timer includes construction and returning geometry; returned
|
||||
geometry destruction occurs afterwards. Sorted index `ceil((n-1)*p)` supplies
|
||||
reported percentiles. Renderer startup is outside the timed phases.
|
||||
|
||||
`grid-performance-01.log`, produced by `grid_render_probe_v1.rs`, records:
|
||||
|
||||
| Canvas, zoom | Dots | Fresh geometry p50 ms | Cache retrieval p50 ms |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 1900x660, 1.0 | 2,240 | 67.8095 | 0.0001 |
|
||||
| 1900x660, 0.5 | 6,528 | 192.5581 | 0.0001 |
|
||||
| 1440x900, 1.0 | 2,318 | 66.2206 | 0.0002 |
|
||||
| 1440x900, 0.5 | 6,695 | 219.1771 | 0.0002 |
|
||||
|
||||
Cold and warm cached grid pixels equal the uncached result byte for byte in all
|
||||
four cases. Cache misses remain comparable to fresh construction; caching does
|
||||
not improve continuously changing camera inputs. Offscreen screenshot/readback
|
||||
cost remains substantial and variable. It must not be converted to native FPS.
|
||||
|
||||
## Candidate comparison and chosen change
|
||||
|
||||
1. Rebuild each circle every draw: exact current appearance, but the measured
|
||||
repeated CPU cost alone is large in this development build.
|
||||
2. Batch circles into one path: keeps the same pixels in this experiment but is
|
||||
materially slower. `grid_render_probe.rs` and `grid-performance-02.log` retain
|
||||
this rejected candidate. Its construction medians range from 274.66ms to
|
||||
1,225.53ms, versus 63.82–178.24ms for the original loop in the same run.
|
||||
3. Cache background geometry while camera and size stay identical: selected.
|
||||
It preserves the original painter and removes its repeated construction from
|
||||
stationary-camera interactions. It adds a small local cache, not a second
|
||||
geometry implementation or a model-level cache.
|
||||
|
||||
`diagram/grid.rs` owns an Iced geometry cache and an exact raw `(zoom, pan)` key.
|
||||
Iced's cache checks exact frame size. Camera changes explicitly clear it. The
|
||||
colors and dot radius remain compile-time constants; future runtime styling
|
||||
must invalidate this cache too. The conservative key rebuilds after a whole
|
||||
grid-period pan and after zoom changes within the minimum-spacing clamp. No
|
||||
fractional coordinates are rounded or tolerance-compared.
|
||||
|
||||
Drawing order becomes `[cached background and dots, live diagram, warnings]`.
|
||||
The opaque background is in the first geometry, so it cannot cover the dots.
|
||||
Existing pan/zoom transforms still apply only to the live model geometry.
|
||||
The cache lives in the retained canvas State; model revisions and selections
|
||||
do not invalidate it. Existing focus/suspension tree resets cause a safe cold
|
||||
rebuild. No routing, model semantics, hit testing or gesture admission changes.
|
||||
|
||||
`production_grid_probe.rs` imports the actual candidate cache. Its exploratory
|
||||
run 03 was performed before the source commit and formatting; the raw log is
|
||||
retained as local precommit evidence, not relabeled as a binary from the commit.
|
||||
The same source is supplied for reproduction at the identified candidate.
|
||||
|
||||
The separately recompiled run 04 imports the committed `636093aa` cache and ran
|
||||
after the full desktop test process exited, before Clippy/build compilation.
|
||||
`grid-performance-04.log` records exact baseline/cached pixels and these medians:
|
||||
|
||||
| Canvas, zoom | Fresh geometry ms | Production cache hit ms | Changing-pan rebuild ms |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 1900x660, 1.0 | 61.8281 | 0.0002 | 59.2995 |
|
||||
| 1900x660, 0.5 | 158.8603 | 0.0002 | 163.4359 |
|
||||
| 1440x900, 1.0 | 50.5385 | 0.0002 | 53.5840 |
|
||||
| 1440x900, 0.5 | 168.4092 | 0.0002 | 175.2457 |
|
||||
|
||||
These results support the same bounded conclusion as the exploratory trials.
|
||||
The run record preserves linked-library, driver and log hashes; timing noise
|
||||
and screenshot readback remain explicit limitations.
|
||||
Run 04 directly compares the production cache's cold pixels; its generic
|
||||
cold-and-warm marker refers to the Iced-cache control. Warm production-cache
|
||||
pixel parity is established separately by the committed grid tests.
|
||||
|
||||
## Verification and boundaries
|
||||
|
||||
Three committed headless tests check:
|
||||
|
||||
- Cache construction count through 18 exact camera/size changes, including
|
||||
negative/fractional pan, zero-pan zoom changes, the minimum-spacing transition,
|
||||
independent width/height resizing and return to previous dimensions.
|
||||
- Exact RGBA equality against an independent copy of the legacy loop and
|
||||
unsplit geometry. The scenes contain opaque cards, crossing edges, text,
|
||||
translucent overdraw, selection handles and a warning overlay. Foreground
|
||||
edits must visibly change pixels while retaining the background cache.
|
||||
- The actual `DiagramCanvas` with one retained State: selection and model
|
||||
revision changes must alter rendered pixels without rebuilding the grid;
|
||||
pan and resize must rebuild it. This is drawing/scene-invalidation evidence,
|
||||
not an event-driven native gesture test.
|
||||
|
||||
Fresh read-only independent review found no concrete cache-key, lifetime,
|
||||
ordering or test-oracle defect. It checked the corresponding Iced 0.14 cache
|
||||
and mesh-layer implementations. This review does not establish execution.
|
||||
At `636093aa`, `grid-tests-01.log` passes all three targeted tests in 2.97s.
|
||||
`grid-desktop-01.log` passes the full desktop suite in 362.14s: 1,543 passed,
|
||||
zero failed, seven existing manual/diagnostic tests ignored, none filtered out.
|
||||
All 1,550 distinct test names were counted; `context.json` lists the seven
|
||||
ignored cases and records raw log hashes. This is desktop regression evidence;
|
||||
it does not relabel the ignored checks as passing or establish native FPS.
|
||||
The [independent review](independent-review.md) states its exact scope.
|
||||
|
||||
The initial strict Clippy run (`grid-clippy-01.log`) failed on two uses of
|
||||
`chunks_exact(4)` in the new test helper. Test-only successor
|
||||
`336720559bcec511949caa8a42bee8a55c3822da` uses `as_chunks::<4>()`; it leaves
|
||||
production inputs unchanged. The original full-suite result remains associated
|
||||
with `636093aa`, not relabeled as execution at the successor.
|
||||
At the successor, strict all-target Clippy passes in 2m01s
|
||||
(`grid-clippy-02.log`) and the three affected grid tests pass again in 4.13s
|
||||
(`grid-tests-02.log`). No warning suppression or test exclusion was introduced.
|
||||
|
||||
The normal Windows GUI build at `33672055` also passes in 2m15s
|
||||
(`desktop-build-02.log`). The executable and required `libunwind.dll` are staged
|
||||
locally for later native verification, but this candidate has not been launched.
|
||||
The executable SHA-256 is
|
||||
`78548e93d4b57130ed507ed0f29eddbdbafb4721b38b0cd6d06101d5324114dd`;
|
||||
`context.json` records both staged file hashes. Staging is not publication of a
|
||||
new runnable release or native acceptance.
|
||||
|
||||
Remaining limits: the user's exact saved scene is unavailable; these results do
|
||||
not establish end-to-end native FPS. Continuous pan/zoom still pays the cold
|
||||
construction cost. Native desktop verification remains paused after user Escape.
|
||||
Neither issue 40 nor issue 44 is closed by this headless experiment. Linux/WSL
|
||||
remains deferred by the user. No published release has been replaced.
|
||||
|
||||
## Environment and reproduction
|
||||
|
||||
The subsequent [cold-construction and pan experiments](COLD-PAN.md) compare
|
||||
additional approaches, including a renderer-translated cache. They preserve
|
||||
their own pixel differences, timing limits and adoption gates; they do not
|
||||
change the production implementation verified above.
|
||||
|
||||
Windows 10.0.26200, Rust 1.98.1, x86_64-pc-windows-gnullvm, development profile
|
||||
with debug information disabled. The exposed CPU is Intel Core i7-10870H with
|
||||
4 cores/threads; Windows reports VMware SVGA 3D, driver 9.17.11.4. The headless
|
||||
renderer identifies itself as `wgpu`; the probe does not identify its selected
|
||||
adapter or equate it with the desktop presenter's adapter. Other OS workloads
|
||||
were not controlled, so measured outliers are preserved.
|
||||
|
||||
At the candidate source, the normal gates are:
|
||||
|
||||
```powershell
|
||||
cargo test --locked --offline -p oa-desktop diagram::grid::tests -- --test-threads=1
|
||||
cargo test --locked --offline -p oa-desktop -- --test-threads=1
|
||||
cargo clippy --locked --offline -p oa-desktop --all-targets -- -D warnings
|
||||
cargo build --locked --offline -p oa-desktop --bin arcadia-studio
|
||||
```
|
||||
|
||||
For the standalone probes, first build the desktop dependencies in that profile.
|
||||
Compile with `rustc --edition=2024 -C opt-level=0`, supplying `--extern iced=...`
|
||||
and `--extern tokio=...` for the matching rlibs, plus `-L dependency=...` for
|
||||
their directory. On this toolchain the link also needs `-C linker=clang.exe`
|
||||
and `-L native=.../windows_x86_64_gnullvm-0.52.6/lib`. The original inputs were
|
||||
`libiced-594f7d417d69ed72.rlib` and `libtokio-5e0ca96735e3073a.rlib` from
|
||||
`target/debug/deps`. Run the resulting executable with argument `20`.
|
||||
The production probe's relative module path requires compiling it at its
|
||||
versioned location. Keep linker and runtime DLLs on PATH.
|
||||
|
||||
The first experimental compile failed because the configured futures crate
|
||||
lacked its executor feature and an inherent screenshot method shadowed the
|
||||
Headless trait method. Using the existing Tokio runtime and explicitly calling
|
||||
`Headless::screenshot` corrected those driver errors. A second link failed for
|
||||
the Windows import library search path; `grid-build-02.log` retains that failure.
|
||||
Adding the native library path allowed subsequent probes to run. These failures
|
||||
occurred in the experimental driver, before execution; they are not passing
|
||||
application checks.
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"application_source": "336720559bcec511949caa8a42bee8a55c3822da",
|
||||
"base_evidence": "2ca84676ee4dc30846208881a27d90a901482085",
|
||||
"production_changes": false,
|
||||
"environment": "Same unoptimized Windows Iced/Tokio artifacts as context.json",
|
||||
"logs": [
|
||||
{
|
||||
"driver": "cold_grid_probe.rs",
|
||||
"exit_code": 0,
|
||||
"log": "grid-cold-01.log"
|
||||
},
|
||||
{
|
||||
"driver": "phase_grid_probe_v1.rs",
|
||||
"exit_code": 0,
|
||||
"compile_warning": "Unused duplicate Renderer trait import; retained in original driver",
|
||||
"log": "grid-phase-01.log"
|
||||
},
|
||||
{
|
||||
"driver": "phase_grid_opaque_probe.rs",
|
||||
"exit_code": 0,
|
||||
"compile_warnings_denied": true,
|
||||
"log": "grid-phase-opaque-01.log"
|
||||
},
|
||||
{
|
||||
"driver": "phase_grid_diagnostic.rs",
|
||||
"exit_code": 0,
|
||||
"compile_warnings_denied": true,
|
||||
"log": "grid-phase-diagnostic-01.log"
|
||||
},
|
||||
{
|
||||
"driver": "phase_grid_replay.rs",
|
||||
"log": "grid-phase-replay-01.log",
|
||||
"exit_code": 0,
|
||||
"compile_warnings_denied": true,
|
||||
"initial_compile": "E0689 ambiguous Vector scalar before rem_euclid, fixed with Vector<f32>; no execution on failed attempt"
|
||||
}
|
||||
],
|
||||
"sha256": {
|
||||
"cold_grid_probe.rs": "a4cc6543229ae4901d54061823a3b05ec6fe3b988f245df12898e013dbb93323",
|
||||
"phase_grid_probe_v1.rs": "c4293e8bb022fae5bffe67002a6f7c867549f2fa8a865b107f39070655bc57fc",
|
||||
"phase_grid_opaque_probe.rs": "43ac5be462ca31f9d1c49554291e08b60bac9de38face7138eaa850e4dcc0ab9",
|
||||
"phase_grid_diagnostic.rs": "8de3c980ac65eef4fcb32b1b121d14aca35b93c704c5b19a440ad9dc7042248c",
|
||||
"grid-cold-01.log": "ca5cac0be409bafcea2c154d42998cbd622c9e898892574bcd5a2df0c1a70886",
|
||||
"grid-phase-01.log": "e0b2e2a3e24d642e1b6c2b1b418f24c952399758159de051f7c1ba7efbb39c4a",
|
||||
"grid-phase-opaque-01.log": "2716cbeb589f4b23a48575680022231c0b94b56ba9c06ec1adbf9e3e6d07f7cd",
|
||||
"grid-phase-diagnostic-01.log": "1dbb0f745169b88c56a21683dba6797a3dbbe753e8dd742bc9ecc8ca24788886",
|
||||
"phase_grid_replay.rs": "96e62ba5d08f7e26d9705d3f90e3785151597b526293439345b59ed842de4971",
|
||||
"grid-phase-replay-01.log": "040034be1b8eb3d4fc010bbe34c5ded47a953b8295bcf2f9440507d1a149da1f"
|
||||
},
|
||||
"native_fps": "unverified"
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
//! Experimental cold-grid candidates; no window or input automation.
|
||||
use iced::advanced::{
|
||||
graphics::geometry::Renderer as _,
|
||||
renderer::{Headless, Renderer as _},
|
||||
};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Frame, Geometry, Path},
|
||||
};
|
||||
use std::{hint::black_box, time::Instant};
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Candidate {
|
||||
Original,
|
||||
Template,
|
||||
Column,
|
||||
Square,
|
||||
}
|
||||
fn draw(r: &Renderer, size: Size, zoom: f32, pan: Vector, candidate: Candidate) -> Geometry {
|
||||
let mut f = Frame::new(r, size);
|
||||
f.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
let circle = Path::circle(Point::ORIGIN, 0.65);
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
if matches!(candidate, Candidate::Column) {
|
||||
let p = Path::new(|p| {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
p.circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65);
|
||||
p.close();
|
||||
}
|
||||
});
|
||||
f.fill(&p, DOT);
|
||||
continue;
|
||||
}
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
let center = Point::new(ox + x as f32 * step, oy + y as f32 * step);
|
||||
match candidate {
|
||||
Candidate::Original => f.fill(&Path::circle(center, 0.65), DOT),
|
||||
Candidate::Template => f.with_save(|f| {
|
||||
f.translate(Vector::new(center.x, center.y));
|
||||
f.fill(&circle, DOT);
|
||||
}),
|
||||
Candidate::Square => f.fill_rectangle(
|
||||
center - Vector::new(0.575, 0.575),
|
||||
Size::new(1.15, 1.15),
|
||||
DOT,
|
||||
),
|
||||
Candidate::Column => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
f.into_geometry()
|
||||
}
|
||||
fn pixels(r: &mut Renderer, size: Size, g: Geometry) -> Vec<u8> {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
r.draw_geometry(g);
|
||||
Headless::screenshot(r, Size::new(size.width as u32, size.height as u32), 1., BG)
|
||||
}
|
||||
fn main() {
|
||||
let n = std::env::args()
|
||||
.nth(1)
|
||||
.map(|s| s.parse::<usize>().unwrap())
|
||||
.unwrap_or(20)
|
||||
.max(1);
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut r = rt
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
println!(
|
||||
"renderer={} n={n}; construction only, unoptimized linked dependencies; NOT native FPS",
|
||||
r.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
] {
|
||||
println!("size={}x{} zoom={zoom}", size.width, size.height);
|
||||
for candidate in [
|
||||
Candidate::Original,
|
||||
Candidate::Template,
|
||||
Candidate::Column,
|
||||
Candidate::Square,
|
||||
] {
|
||||
for pan in [
|
||||
Vector::new(-13.25, 9.75),
|
||||
Vector::new(0., 0.),
|
||||
Vector::new(6.125, -8.5),
|
||||
] {
|
||||
let base = draw(&r, size, zoom, pan, Candidate::Original);
|
||||
let base = pixels(&mut r, size, base);
|
||||
let test = draw(&r, size, zoom, pan, candidate);
|
||||
let test = pixels(&mut r, size, test);
|
||||
let changed = base
|
||||
.chunks_exact(4)
|
||||
.zip(test.chunks_exact(4))
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
let max = base
|
||||
.iter()
|
||||
.zip(&test)
|
||||
.map(|(a, b)| a.abs_diff(*b))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
println!(
|
||||
"{candidate:?} pan={},{} changed_pixels={changed} max_channel_delta={max}",
|
||||
pan.x, pan.y
|
||||
);
|
||||
}
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..n + 3 {
|
||||
let pan = Vector::new(-13.25 + i as f32 * 0.25, 9.75);
|
||||
let now = Instant::now();
|
||||
let g = black_box(draw(&r, size, zoom, pan, candidate));
|
||||
let elapsed = now.elapsed().as_secs_f64() * 1000.;
|
||||
drop(g);
|
||||
if i >= 3 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"{candidate:?} changing_pan n={n} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
ms[((n - 1) as f64 * 0.5).ceil() as usize],
|
||||
ms[((n - 1) as f64 * 0.95).ceil() as usize],
|
||||
ms[n - 1]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"source": "636093aae67aa48fa1d35e403a205227e81d04de",
|
||||
"target": "x86_64-pc-windows-gnullvm",
|
||||
"rustc": "1.98.1",
|
||||
"rust_llvm": "22.1.8",
|
||||
"linker_clang": "23.1.1",
|
||||
"profile": "dev/test unoptimized debug=0",
|
||||
"incremental": false,
|
||||
"build_jobs": 1,
|
||||
"full_test_count": 1550,
|
||||
"passed": 1543,
|
||||
"ignored": 7,
|
||||
"failed": 0,
|
||||
"ignored_tests": [
|
||||
"app::source_rename::timing_tests::accepted_release1_prepare_commit_sync_and_confirm_timing",
|
||||
"app::source_rename::timing_tests::layout_timing_tests::accepted_release1_constructor_vs_reuse_through_next_highlight",
|
||||
"app::source_symbols::tests::source_symbols_100_modules_1000_definitions_10000_uses",
|
||||
"behavior::scenario::spacing::controller::timing_tests::saved_spacing_capture_prepare_20_lanes_1000_messages_3_views",
|
||||
"source_annotations::anchor_tests::final_multi_glyph_cluster_anchor_uses_logical_trailing_advance_edge",
|
||||
"source_annotations::anchor_tests::mixed_direction_omitted_separator_has_one_logical_predecessor_anchor",
|
||||
"source_annotations::performance_tests::annotation_projection_cost_distributions"
|
||||
],
|
||||
"log_sha256": {
|
||||
"grid-tests-01.log": "ed76ce90e9246dfddf1d3a96d6a1b91d0d0719701b1d09ddb639a1277c089612",
|
||||
"grid-desktop-01.log": "2000cf03685d6fde73c3dede90f84153a45c6ad0a5c2886a2bc09dda98ba63c3",
|
||||
"grid-performance-04.log": "c492aba0bbe600fa1c2a7f0ac52e5706d29cb67aca95fa3a31641ff7f90c2412",
|
||||
"grid-clippy-01.log": "2498a3d0be9738b8e43ae588c23d6e48f531fdbfc5f47d2f79c1a2f0f1b5bdc1",
|
||||
"grid-clippy-02.log": "69de93cce0c53f37f3c82eb28a28c1610548b30fec4680f02b9b7b118606040f",
|
||||
"grid-tests-02.log": "54c1d72402310be8b3026886dfda0339aa1a81c76b8165c40c97ea075524bff2",
|
||||
"desktop-build-02.log": "e39d1eaaceb8a5805a486c204d6eca7e1ce2b9b39b232e770ab0ad33cbe0e860"
|
||||
},
|
||||
"native_acceptance": "unverified",
|
||||
"source_clean_after_full_test": true,
|
||||
"timing_run": {
|
||||
"source": "636093aae67aa48fa1d35e403a205227e81d04de",
|
||||
"samples": 20,
|
||||
"warmups": 3,
|
||||
"probe_exe_sha256": "79c2e0b64cfacf24b7ee10d8690969d961753945f45d16a743ea750b9eb049d0",
|
||||
"probe_source_sha256": "e6efe1e510164444912d05a96e49c55d791b7046102013458a5ad94bd474a6cd",
|
||||
"grid_git_blob_sha256": "5e7c54d1b4da2e2c082f8ef011a3755f8808fcd67f1babf3be5448cd94240e6c",
|
||||
"iced_rlib_sha256": "9fec4bea1a5ac7e1ae75d786b76e3dbe97a6f63b30d11b6753f268c2ad9efbc6",
|
||||
"tokio_rlib_sha256": "78587ae94762065914b25065c85d9cb66106b71619ec7b65f5e16cdeb689abb9"
|
||||
},
|
||||
"test_only_successor": {
|
||||
"source": "336720559bcec511949caa8a42bee8a55c3822da",
|
||||
"change": "chunks_exact(4) replaced by as_chunks::<4>() in the pixel test helper",
|
||||
"targeted_passed": 3,
|
||||
"targeted_failed": 0,
|
||||
"targeted_filtered": 1547,
|
||||
"strict_clippy": "passed",
|
||||
"normal_build": "passed"
|
||||
},
|
||||
"staged_candidate": {
|
||||
"source": "336720559bcec511949caa8a42bee8a55c3822da",
|
||||
"launched": false,
|
||||
"files_sha256": {
|
||||
"arcadia-studio.exe": "78548e93d4b57130ed507ed0f29eddbdbafb4721b38b0cd6d06101d5324114dd",
|
||||
"libunwind.dll": "aa282fa47bb2c4e210cea5e470cdbdca58568f0be0bc73e404d86ab9cd95ef6a"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `dev` profile [unoptimized] target(s) in 2m 15s
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,25 @@
|
||||
Checking oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
error: using `chunks_exact` with a constant chunk size
|
||||
--> crates\oa-desktop\src\diagram\grid\tests.rs:231:10
|
||||
|
|
||||
231 | .chunks_exact(4)
|
||||
| ^^^^^^^^^^^^^^^ help: consider using `as_chunks` instead: `as_chunks::<4>().0.iter()`
|
||||
|
|
||||
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#chunks_exact_to_as_chunks
|
||||
= note: `-D clippy::chunks-exact-to-as-chunks` implied by `-D warnings`
|
||||
= help: to override `-D warnings` add `#[allow(clippy::chunks_exact_to_as_chunks)]`
|
||||
|
||||
error: using `chunks_exact` with a constant chunk size
|
||||
--> crates\oa-desktop\src\diagram\grid\tests.rs:232:21
|
||||
|
|
||||
232 | .zip(actual.chunks_exact(4))
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
help: consider using `as_chunks::<4>()` instead
|
||||
--> crates\oa-desktop\src\diagram\grid\tests.rs:232:21
|
||||
|
|
||||
232 | .zip(actual.chunks_exact(4))
|
||||
| ^^^^^^^^^^^^^^^
|
||||
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#chunks_exact_to_as_chunks
|
||||
|
||||
error: could not compile `oa-desktop` (bin "arcadia-studio" test) due to 2 previous errors
|
||||
@@ -0,0 +1,2 @@
|
||||
Checking oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `dev` profile [unoptimized] target(s) in 2m 01s
|
||||
@@ -0,0 +1,69 @@
|
||||
renderer=wgpu n=20; construction only, unoptimized linked dependencies; NOT native FPS
|
||||
size=1900x660 zoom=1
|
||||
Original pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Original changing_pan n=20 p50_ms=61.0564 p95_ms=111.2137 max_ms=111.2137
|
||||
Template pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Template changing_pan n=20 p50_ms=55.3975 p95_ms=73.8790 max_ms=73.8790
|
||||
Column pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Column changing_pan n=20 p50_ms=54.3594 p95_ms=69.6607 max_ms=69.6607
|
||||
Square pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=6.125,-8.5 changed_pixels=4266 max_channel_delta=7
|
||||
Square changing_pan n=20 p50_ms=3.5527 p95_ms=5.6911 max_ms=5.6911
|
||||
size=1900x660 zoom=0.5
|
||||
Original pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Original changing_pan n=20 p50_ms=215.8919 p95_ms=253.9666 max_ms=253.9666
|
||||
Template pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Template changing_pan n=20 p50_ms=162.3073 p95_ms=212.2833 max_ms=212.2833
|
||||
Column pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Column changing_pan n=20 p50_ms=158.5968 p95_ms=449.6066 max_ms=449.6066
|
||||
Square pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=6.125,-8.5 changed_pixels=12784 max_channel_delta=7
|
||||
Square changing_pan n=20 p50_ms=7.5699 p95_ms=13.3416 max_ms=13.3416
|
||||
size=1440x900 zoom=1
|
||||
Original pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Original changing_pan n=20 p50_ms=60.1683 p95_ms=88.4967 max_ms=88.4967
|
||||
Template pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Template changing_pan n=20 p50_ms=43.3860 p95_ms=53.1866 max_ms=53.1866
|
||||
Column pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Column changing_pan n=20 p50_ms=60.8198 p95_ms=80.8932 max_ms=80.8932
|
||||
Square pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=6.125,-8.5 changed_pixels=4440 max_channel_delta=7
|
||||
Square changing_pan n=20 p50_ms=2.0340 p95_ms=4.0620 max_ms=4.0620
|
||||
size=1440x900 zoom=0.5
|
||||
Original pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Original pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Original changing_pan n=20 p50_ms=165.2049 p95_ms=203.5331 max_ms=203.5331
|
||||
Template pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Template pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Template changing_pan n=20 p50_ms=133.3420 p95_ms=143.9790 max_ms=143.9790
|
||||
Column pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Column pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
Column changing_pan n=20 p50_ms=143.1489 p95_ms=155.3680 max_ms=155.3680
|
||||
Square pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
Square pan=6.125,-8.5 changed_pixels=13184 max_channel_delta=7
|
||||
Square changing_pan n=20 p50_ms=6.8178 p95_ms=10.8775 max_ms=10.8775
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
renderer=wgpu samples=20; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation
|
||||
|
||||
size=1900x660 zoom=1 dots=2240
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=67.8095 p95_ms=77.2227 max_ms=77.2227
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0002 max_ms=0.0002
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=171.4916 p95_ms=231.2029 max_ms=231.2029
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=99.1916 p95_ms=125.0517 max_ms=125.0517
|
||||
cache_forced_miss_geometry: n=20 p50_ms=65.6279 p95_ms=75.6531 max_ms=75.6531
|
||||
|
||||
size=1900x660 zoom=0.5 dots=6528
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=192.5581 p95_ms=237.9956 max_ms=237.9956
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0009 max_ms=0.0009
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=328.0257 p95_ms=498.0908 max_ms=498.0908
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=108.5102 p95_ms=119.6021 max_ms=119.6021
|
||||
cache_forced_miss_geometry: n=20 p50_ms=183.2740 p95_ms=212.6681 max_ms=212.6681
|
||||
|
||||
size=1440x900 zoom=1 dots=2318
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=66.2206 p95_ms=89.5746 max_ms=89.5746
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0010 max_ms=0.0010
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=194.1484 p95_ms=445.2525 max_ms=445.2525
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=113.8430 p95_ms=152.8658 max_ms=152.8658
|
||||
cache_forced_miss_geometry: n=20 p50_ms=81.6705 p95_ms=200.7735 max_ms=200.7735
|
||||
|
||||
size=1440x900 zoom=0.5 dots=6695
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=219.1771 p95_ms=270.8434 max_ms=270.8434
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0003 max_ms=0.0003
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=387.7051 p95_ms=600.4906 max_ms=600.4906
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=119.9417 p95_ms=157.2978 max_ms=157.2978
|
||||
cache_forced_miss_geometry: n=20 p50_ms=193.8187 p95_ms=222.6229 max_ms=222.6229
|
||||
@@ -0,0 +1,41 @@
|
||||
renderer=wgpu samples=20; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation
|
||||
|
||||
size=1900x660 zoom=1 dots=2240
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=63.8161 p95_ms=74.3558 max_ms=74.3558
|
||||
batched_geometry: n=20 p50_ms=280.9291 p95_ms=361.9218 max_ms=361.9218
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0004 max_ms=0.0004
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=203.0484 p95_ms=314.9968 max_ms=314.9968
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=109.2162 p95_ms=470.8047 max_ms=470.8047
|
||||
cache_forced_miss_geometry: n=20 p50_ms=73.1544 p95_ms=112.9079 max_ms=112.9079
|
||||
|
||||
size=1900x660 zoom=0.5 dots=6528
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=178.2363 p95_ms=204.1913 max_ms=204.1913
|
||||
batched_geometry: n=20 p50_ms=1225.5328 p95_ms=1791.5758 max_ms=1791.5758
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0008 max_ms=0.0008
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=281.4035 p95_ms=308.5933 max_ms=308.5933
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=100.8220 p95_ms=114.5026 max_ms=114.5026
|
||||
cache_forced_miss_geometry: n=20 p50_ms=179.6717 p95_ms=319.2162 max_ms=319.2162
|
||||
|
||||
size=1440x900 zoom=1 dots=2318
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=67.1604 p95_ms=76.3298 max_ms=76.3298
|
||||
batched_geometry: n=20 p50_ms=274.6607 p95_ms=551.4707 max_ms=551.4707
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0003 max_ms=0.0003
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=168.4817 p95_ms=294.9456 max_ms=294.9456
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=92.0521 p95_ms=105.3303 max_ms=105.3303
|
||||
cache_forced_miss_geometry: n=20 p50_ms=70.1733 p95_ms=83.6053 max_ms=83.6053
|
||||
|
||||
size=1440x900 zoom=0.5 dots=6695
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=177.7526 p95_ms=205.8862 max_ms=205.8862
|
||||
batched_geometry: n=20 p50_ms=1012.6393 p95_ms=1103.8729 max_ms=1103.8729
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0006 max_ms=0.0006
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=293.3138 p95_ms=315.0118 max_ms=315.0118
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=110.6309 p95_ms=127.6823 max_ms=127.6823
|
||||
cache_forced_miss_geometry: n=20 p50_ms=195.3297 p95_ms=327.8771 max_ms=327.8771
|
||||
@@ -0,0 +1,45 @@
|
||||
renderer=wgpu samples=20; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation
|
||||
|
||||
size=1900x660 zoom=1 dots=2240
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=70.2238 p95_ms=101.8816 max_ms=101.8816
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0005 max_ms=0.0005
|
||||
production_changing_pan_geometry: n=20 p50_ms=74.2265 p95_ms=150.4436 max_ms=150.4436
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0002 max_ms=0.0002
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=179.3216 p95_ms=272.4518 max_ms=272.4518
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=108.8949 p95_ms=138.2430 max_ms=138.2430
|
||||
cache_forced_miss_geometry: n=20 p50_ms=73.2982 p95_ms=229.4395 max_ms=229.4395
|
||||
|
||||
size=1900x660 zoom=0.5 dots=6528
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=190.5914 p95_ms=245.6332 max_ms=245.6332
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0004 max_ms=0.0004
|
||||
production_changing_pan_geometry: n=20 p50_ms=200.1888 p95_ms=243.7068 max_ms=243.7068
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0002 max_ms=0.0002
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=356.6151 p95_ms=438.3689 max_ms=438.3689
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=151.2161 p95_ms=236.6296 max_ms=236.6296
|
||||
cache_forced_miss_geometry: n=20 p50_ms=198.4648 p95_ms=312.0322 max_ms=312.0322
|
||||
|
||||
size=1440x900 zoom=1 dots=2318
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=83.2017 p95_ms=284.6453 max_ms=284.6453
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0010 max_ms=0.0010
|
||||
production_changing_pan_geometry: n=20 p50_ms=61.6783 p95_ms=79.8293 max_ms=79.8293
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0003 max_ms=0.0003
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=168.7804 p95_ms=206.0726 max_ms=206.0726
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=114.7579 p95_ms=130.0840 max_ms=130.0840
|
||||
cache_forced_miss_geometry: n=20 p50_ms=103.6019 p95_ms=215.1691 max_ms=215.1691
|
||||
|
||||
size=1440x900 zoom=0.5 dots=6695
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=202.9283 p95_ms=247.2353 max_ms=247.2353
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0004 max_ms=0.0004
|
||||
production_changing_pan_geometry: n=20 p50_ms=223.5437 p95_ms=243.7359 max_ms=243.7359
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0002 max_ms=0.0002
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=299.4320 p95_ms=359.2924 max_ms=359.2924
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=113.6960 p95_ms=180.7261 max_ms=180.7261
|
||||
cache_forced_miss_geometry: n=20 p50_ms=191.9370 p95_ms=294.2203 max_ms=294.2203
|
||||
@@ -0,0 +1,45 @@
|
||||
renderer=wgpu samples=20; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation
|
||||
|
||||
size=1900x660 zoom=1 dots=2240
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=61.8281 p95_ms=68.9849 max_ms=68.9849
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0002 max_ms=0.0002
|
||||
production_changing_pan_geometry: n=20 p50_ms=59.2995 p95_ms=69.6670 max_ms=69.6670
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0029 max_ms=0.0029
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=136.3939 p95_ms=160.0453 max_ms=160.0453
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=82.0448 p95_ms=105.5853 max_ms=105.5853
|
||||
cache_forced_miss_geometry: n=20 p50_ms=52.1529 p95_ms=58.0832 max_ms=58.0832
|
||||
|
||||
size=1900x660 zoom=0.5 dots=6528
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=158.8603 p95_ms=169.1878 max_ms=169.1878
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0002 max_ms=0.0002
|
||||
production_changing_pan_geometry: n=20 p50_ms=163.4359 p95_ms=262.5349 max_ms=262.5349
|
||||
cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0002 max_ms=0.0002
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=226.3661 p95_ms=247.0635 max_ms=247.0635
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=78.5761 p95_ms=99.7537 max_ms=99.7537
|
||||
cache_forced_miss_geometry: n=20 p50_ms=154.7299 p95_ms=177.8883 max_ms=177.8883
|
||||
|
||||
size=1440x900 zoom=1 dots=2318
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=50.5385 p95_ms=57.4277 max_ms=57.4277
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0002 max_ms=0.0002
|
||||
production_changing_pan_geometry: n=20 p50_ms=53.5840 p95_ms=60.8569 max_ms=60.8569
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0005 max_ms=0.0005
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=124.3827 p95_ms=140.3617 max_ms=140.3617
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=75.7445 p95_ms=97.7246 max_ms=97.7246
|
||||
cache_forced_miss_geometry: n=20 p50_ms=54.7114 p95_ms=61.8326 max_ms=61.8326
|
||||
|
||||
size=1440x900 zoom=0.5 dots=6695
|
||||
batched_rgba_byte_differences=0
|
||||
pixel_parity=cold_and_warm_exact
|
||||
fresh_geometry: n=20 p50_ms=168.4092 p95_ms=186.4184 max_ms=186.4184
|
||||
production_cached_geometry: n=20 p50_ms=0.0002 p95_ms=0.0003 max_ms=0.0003
|
||||
production_changing_pan_geometry: n=20 p50_ms=175.2457 p95_ms=205.4313 max_ms=205.4313
|
||||
cached_geometry: n=20 p50_ms=0.0001 p95_ms=0.0002 max_ms=0.0002
|
||||
fresh_geometry_and_screenshot: n=20 p50_ms=253.4003 p95_ms=420.1746 max_ms=420.1746
|
||||
cached_geometry_and_screenshot: n=20 p50_ms=85.1261 p95_ms=107.0600 max_ms=107.0600
|
||||
cache_forced_miss_geometry: n=20 p50_ms=155.4512 p95_ms=163.8325 max_ms=163.8325
|
||||
@@ -0,0 +1,96 @@
|
||||
renderer=wgpu; phase-cache prototype; NOT native FPS
|
||||
size=1900x660 zoom=1 step=24
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=2212 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=2133 max_channel_delta=1
|
||||
scale=1 pan=23.875,23.75 changed_pixels=2133 max_channel_delta=1
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=4424 max_channel_delta=1
|
||||
scale=1.25 pan=0,0 changed_pixels=8745 max_channel_delta=1
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=2212 max_channel_delta=1
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=23.875,23.75 changed_pixels=4266 max_channel_delta=1
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=6557 max_channel_delta=1
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=8848 max_channel_delta=1
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=4266 max_channel_delta=1
|
||||
scale=2 pan=23.875,23.75 changed_pixels=10665 max_channel_delta=1
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=6478 max_channel_delta=1
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=66.6997 p95_ms=77.3888 max_ms=77.3888
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0010 p95_ms=0.0017 max_ms=0.0017
|
||||
cache_builds=1
|
||||
size=1900x660 zoom=0.5 step=14
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=6392 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=6392 max_channel_delta=1
|
||||
scale=1 pan=13.875,13.75 changed_pixels=6345 max_channel_delta=1
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=12960 max_channel_delta=1
|
||||
scale=1.25 pan=0,0 changed_pixels=12781 max_channel_delta=1
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=9588 max_channel_delta=1
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=11288 max_channel_delta=1
|
||||
scale=1.25 pan=13.875,13.75 changed_pixels=9494 max_channel_delta=1
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=12801 max_channel_delta=1
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=25568 max_channel_delta=1
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=12784 max_channel_delta=1
|
||||
scale=2 pan=13.875,13.75 changed_pixels=31725 max_channel_delta=1
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=19170 max_channel_delta=1
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=189.4570 p95_ms=221.8240 max_ms=221.8240
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0007 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
size=1440x900 zoom=1 step=24
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=2280 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=2220 max_channel_delta=1
|
||||
scale=1 pan=23.875,23.75 changed_pixels=2220 max_channel_delta=1
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=4522 max_channel_delta=1
|
||||
scale=1.25 pan=0,0 changed_pixels=9000 max_channel_delta=1
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=2280 max_channel_delta=1
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=23.875,23.75 changed_pixels=4403 max_channel_delta=1
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=6742 max_channel_delta=1
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=9120 max_channel_delta=1
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=4440 max_channel_delta=1
|
||||
scale=2 pan=23.875,23.75 changed_pixels=11026 max_channel_delta=1
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=6683 max_channel_delta=1
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=66.8644 p95_ms=93.9216 max_ms=93.9216
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
size=1440x900 zoom=0.5 step=14
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=6592 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=6592 max_channel_delta=1
|
||||
scale=1 pan=13.875,13.75 changed_pixels=6528 max_channel_delta=1
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=13260 max_channel_delta=1
|
||||
scale=1.25 pan=0,0 changed_pixels=13306 max_channel_delta=1
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=9952 max_channel_delta=1
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=11488 max_channel_delta=1
|
||||
scale=1.25 pan=13.875,13.75 changed_pixels=9792 max_channel_delta=1
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=13209 max_channel_delta=1
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=26368 max_channel_delta=1
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=13184 max_channel_delta=1
|
||||
scale=2 pan=13.875,13.75 changed_pixels=32640 max_channel_delta=1
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=19686 max_channel_delta=1
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=202.5700 p95_ms=482.7433 max_ms=482.7433
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0011 p95_ms=0.0041 max_ms=0.0041
|
||||
cache_builds=1
|
||||
size=301.5x211.25 zoom=0.73 step=17.52
|
||||
scale=1 pan=0,0 changed_pixels=263 max_channel_delta=1
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=251 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=250 max_channel_delta=1
|
||||
scale=1 pan=17.395,17.27 changed_pixels=238 max_channel_delta=1
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=241 max_channel_delta=1
|
||||
scale=1.25 pan=0,0 changed_pixels=379 max_channel_delta=1
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=395 max_channel_delta=1
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=332 max_channel_delta=1
|
||||
scale=1.25 pan=17.395,17.27 changed_pixels=289 max_channel_delta=1
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=372 max_channel_delta=1
|
||||
scale=2 pan=0,0 changed_pixels=759 max_channel_delta=1
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=626 max_channel_delta=6
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=731 max_channel_delta=1
|
||||
scale=2 pan=17.395,17.27 changed_pixels=576 max_channel_delta=1
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=784 max_channel_delta=1
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=6.6260 p95_ms=21.1743 max_ms=21.1743
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0010 p95_ms=0.0012 max_ms=0.0012
|
||||
cache_builds=1
|
||||
@@ -0,0 +1,32 @@
|
||||
renderer=wgpu; phase-cache prototype; NOT native FPS
|
||||
size=301.5x211.25 zoom=0.73 step=17.52
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
difference_xy=301,26 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,27 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,62 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,80 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,97 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,115 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,132 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,150 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,167 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,184 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,185 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,202 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=12 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=6.1755 p95_ms=8.4025 max_ms=8.4025
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0011 p95_ms=0.0019 max_ms=0.0019
|
||||
cache_builds=1
|
||||
@@ -0,0 +1,100 @@
|
||||
renderer=wgpu; phase-cache prototype; NOT native FPS
|
||||
size=1900x660 zoom=1 step=24
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=72.0271 p95_ms=99.9725 max_ms=99.9725
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0011 p95_ms=0.0015 max_ms=0.0015
|
||||
cache_builds=1
|
||||
cached=false construct_submit_screenshot changing_pan n=8 p50_ms=259.9265 min_ms=253.3506 max_ms=281.4865
|
||||
cached=true construct_submit_screenshot changing_pan n=8 p50_ms=212.6536 min_ms=184.5191 max_ms=299.2711
|
||||
size=1900x660 zoom=0.5 step=14
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=167.4776 p95_ms=186.8968 max_ms=186.8968
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
size=1440x900 zoom=1 step=24
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=68.4232 p95_ms=97.6147 max_ms=97.6147
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0458 max_ms=0.0458
|
||||
cache_builds=1
|
||||
size=1440x900 zoom=0.5 step=14
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=173.7177 p95_ms=215.3188 max_ms=215.3188
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0009 max_ms=0.0009
|
||||
cache_builds=1
|
||||
cached=false construct_submit_screenshot changing_pan n=8 p50_ms=337.2560 min_ms=319.0543 max_ms=366.4956
|
||||
cached=true construct_submit_screenshot changing_pan n=8 p50_ms=170.3780 min_ms=144.1992 max_ms=188.4337
|
||||
size=301.5x211.25 zoom=0.73 step=17.52
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=12 max_channel_delta=1
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=1 max_channel_delta=6
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=5.4797 p95_ms=6.8957 max_ms=6.8957
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0007 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
@@ -0,0 +1,127 @@
|
||||
renderer=wgpu; phase-cache prototype; NOT native FPS
|
||||
size=1900x660 zoom=1 step=24
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=68.3424 p95_ms=88.0883 max_ms=88.0883
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0010 p95_ms=0.0012 max_ms=0.0012
|
||||
cache_builds=1
|
||||
cached=false construct_submit_screenshot changing_pan n=8 p50_ms=249.7269 min_ms=216.9378 max_ms=267.1425
|
||||
cached=true construct_submit_screenshot changing_pan n=8 p50_ms=215.6865 min_ms=177.7329 max_ms=262.5719
|
||||
size=1900x660 zoom=0.5 step=14
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=190.0141 p95_ms=231.4193 max_ms=231.4193
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
size=1440x900 zoom=1 step=24
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=23.875,23.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=72.2449 p95_ms=86.5021 max_ms=86.5021
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
size=1440x900 zoom=0.5 step=14
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=13.875,13.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=209.4728 p95_ms=329.0977 max_ms=329.0977
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0008 p95_ms=0.0009 max_ms=0.0009
|
||||
cache_builds=1
|
||||
cached=false construct_submit_screenshot changing_pan n=8 p50_ms=384.7680 min_ms=347.4879 max_ms=420.2894
|
||||
cached=true construct_submit_screenshot changing_pan n=8 p50_ms=164.1611 min_ms=150.1276 max_ms=186.4567
|
||||
size=301.5x211.25 zoom=0.73 step=17.52
|
||||
scale=1 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-13.25,9.75 changed_pixels=12 max_channel_delta=1
|
||||
difference_xy=301,26 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,27 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,62 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,80 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,97 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,115 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,132 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,150 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,167 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,184 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,185 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
difference_xy=301,202 expected=[240, 243, 247, 255] actual=[240, 243, 246, 255]
|
||||
scale=1 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=1 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=1.25 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=0,0 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-13.25,9.75 changed_pixels=1 max_channel_delta=6
|
||||
difference_xy=149,263 expected=[240, 243, 246, 255] actual=[246, 248, 251, 255]
|
||||
scale=2 pan=6.125,-8.5 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=17.395,17.27 changed_pixels=0 max_channel_delta=0
|
||||
scale=2 pan=-0.125,0.125 changed_pixels=0 max_channel_delta=0
|
||||
cached=false construct_and_submit changing_pan n=20 p50_ms=5.5680 p95_ms=7.0634 max_ms=7.0634
|
||||
cached=true construct_and_submit changing_pan n=20 p50_ms=0.0007 p95_ms=0.0008 max_ms=0.0008
|
||||
cache_builds=1
|
||||
original_sequence_complete; beginning same-renderer repeat controls
|
||||
comparison=old/old repetition=1 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=cached/cached repetition=1 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=old/old repetition=1 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=cached/cached repetition=1 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=old/old repetition=2 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=cached/cached repetition=2 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=old/old repetition=2 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=cached/cached repetition=2 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=old/old repetition=3 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=cached/cached repetition=3 size=301.5x211.25 scale=1 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=old/old repetition=3 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
comparison=cached/cached repetition=3 size=301.5x211.25 scale=2 pan=-13.25,9.75 changed_pixels=0 max_channel_delta=0
|
||||
control_cache_builds=1
|
||||
@@ -0,0 +1,11 @@
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `test` profile [unoptimized] target(s) in 4m 23s
|
||||
Running unittests src\main.rs (C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\arcadia_studio-c5b42126ba36cd38.exe)
|
||||
|
||||
running 3 tests
|
||||
test diagram::grid::tests::actual_canvas_keeps_background_cached_while_selection_and_revision_change ... ok
|
||||
test diagram::grid::tests::cache_reuses_unchanged_inputs_and_rebuilds_for_each_camera_or_size_change ... ok
|
||||
test diagram::grid::tests::cached_background_preserves_pixels_and_live_foreground_across_camera_and_size_changes ... ok
|
||||
|
||||
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 1547 filtered out; finished in 2.97s
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
Compiling oa-desktop v0.2.0 (C:\Users\User\Documents\Git\ARCADIA-Studio\.local\drag-out-01a0d6fd\crates\oa-desktop)
|
||||
Finished `test` profile [unoptimized] target(s) in 3m 31s
|
||||
Running unittests src\main.rs (C:/Users/User/Documents/Git/ARCADIA-Studio/target\debug\deps\arcadia_studio-c5b42126ba36cd38.exe)
|
||||
|
||||
running 3 tests
|
||||
test diagram::grid::tests::actual_canvas_keeps_background_cached_while_selection_and_revision_change ... ok
|
||||
test diagram::grid::tests::cache_reuses_unchanged_inputs_and_rebuilds_for_each_camera_or_size_change ... ok
|
||||
test diagram::grid::tests::cached_background_preserves_pixels_and_live_foreground_across_camera_and_size_changes ... ok
|
||||
|
||||
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 1547 filtered out; finished in 4.13s
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
//! Headless rendering experiment; creates no desktop window and sends no input.
|
||||
use iced::advanced::{
|
||||
graphics::geometry::Renderer as _,
|
||||
renderer::{Headless, Renderer as _},
|
||||
};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
|
||||
fn paint(frame: &mut Frame, size: Size, zoom: f32, pan: Vector) {
|
||||
frame.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
frame.fill(
|
||||
&Path::circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn fresh(renderer: &Renderer, size: Size, zoom: f32, pan: Vector) -> Geometry {
|
||||
let mut frame = Frame::new(renderer, size);
|
||||
paint(&mut frame, size, zoom, pan);
|
||||
frame.into_geometry()
|
||||
}
|
||||
fn batched(renderer: &Renderer, size: Size, zoom: f32, pan: Vector) -> Geometry {
|
||||
let mut frame = Frame::new(renderer, size);
|
||||
frame.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
let path = Path::new(|p| {
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
p.circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65);
|
||||
p.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
frame.fill(&path, DOT);
|
||||
frame.into_geometry()
|
||||
}
|
||||
fn screenshot(renderer: &mut Renderer, size: Size, geometry: Geometry) -> Vec<u8> {
|
||||
renderer.reset(Rectangle::with_size(size));
|
||||
renderer.draw_geometry(geometry);
|
||||
Headless::screenshot(
|
||||
renderer,
|
||||
Size::new(size.width as u32, size.height as u32),
|
||||
1.,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
fn measure<T>(label: &str, n: usize, mut f: impl FnMut() -> T) {
|
||||
for _ in 0..3 {
|
||||
black_box(f());
|
||||
}
|
||||
let mut values = Vec::new();
|
||||
for _ in 0..n {
|
||||
let now = Instant::now();
|
||||
let value = black_box(f());
|
||||
values.push(now.elapsed().as_secs_f64() * 1000.);
|
||||
drop(value);
|
||||
}
|
||||
values.sort_by(f64::total_cmp);
|
||||
let at = |p: f64| values[((n - 1) as f64 * p).ceil() as usize];
|
||||
println!(
|
||||
"{label}: n={n} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
at(0.5),
|
||||
at(0.95),
|
||||
values[n - 1]
|
||||
);
|
||||
}
|
||||
fn main() {
|
||||
let n = std::env::args()
|
||||
.nth(1)
|
||||
.map(|s| s.parse::<usize>().unwrap())
|
||||
.unwrap_or(20)
|
||||
.max(1);
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut renderer = runtime
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.expect("headless production renderer");
|
||||
println!(
|
||||
"renderer={} samples={n}; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation",
|
||||
renderer.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
] {
|
||||
let pan = Vector::new(-13.25, 9.75);
|
||||
let cache = Cache::new();
|
||||
let calls = Cell::new(0);
|
||||
let cached = |r: &Renderer| {
|
||||
cache.draw(r, size, |f| {
|
||||
calls.set(calls.get() + 1);
|
||||
paint(f, size, zoom, pan);
|
||||
})
|
||||
};
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
println!(
|
||||
"\nsize={}x{} zoom={zoom} dots={}",
|
||||
size.width,
|
||||
size.height,
|
||||
((size.width / step) as i32 + 1) * ((size.height / step) as i32 + 1)
|
||||
);
|
||||
let baseline = fresh(&renderer, size, zoom, pan);
|
||||
let baseline_pixels = screenshot(&mut renderer, size, baseline);
|
||||
let geometry = batched(&renderer, size, zoom, pan);
|
||||
let pixels = screenshot(&mut renderer, size, geometry);
|
||||
let differences = baseline_pixels
|
||||
.iter()
|
||||
.zip(&pixels)
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
println!("batched_rgba_byte_differences={differences}");
|
||||
assert_eq!(
|
||||
differences, 0,
|
||||
"batched path must preserve exact grid pixels"
|
||||
);
|
||||
let geometry = cached(&renderer);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry));
|
||||
let geometry = cached(&renderer);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry));
|
||||
assert_eq!(calls.get(), 1);
|
||||
println!("pixel_parity=cold_and_warm_exact");
|
||||
measure("fresh_geometry", n, || fresh(&renderer, size, zoom, pan));
|
||||
measure("batched_geometry", n, || {
|
||||
batched(&renderer, size, zoom, pan)
|
||||
});
|
||||
measure("cached_geometry", n, || cached(&renderer));
|
||||
assert_eq!(calls.get(), 1, "identical camera must reuse geometry");
|
||||
measure("fresh_geometry_and_screenshot", n, || {
|
||||
let g = fresh(&renderer, size, zoom, pan);
|
||||
screenshot(&mut renderer, size, g)
|
||||
});
|
||||
measure("cached_geometry_and_screenshot", n, || {
|
||||
let g = cached(&renderer);
|
||||
screenshot(&mut renderer, size, g)
|
||||
});
|
||||
measure("cache_forced_miss_geometry", n, || {
|
||||
cache.clear();
|
||||
cached(&renderer)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//! Headless rendering experiment; creates no desktop window and sends no input.
|
||||
use iced::advanced::{
|
||||
graphics::geometry::Renderer as _,
|
||||
renderer::{Headless, Renderer as _},
|
||||
};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
|
||||
fn paint(frame: &mut Frame, size: Size, zoom: f32, pan: Vector) {
|
||||
frame.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
frame.fill(
|
||||
&Path::circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn fresh(renderer: &Renderer, size: Size, zoom: f32, pan: Vector) -> Geometry {
|
||||
let mut frame = Frame::new(renderer, size);
|
||||
paint(&mut frame, size, zoom, pan);
|
||||
frame.into_geometry()
|
||||
}
|
||||
fn screenshot(renderer: &mut Renderer, size: Size, geometry: Geometry) -> Vec<u8> {
|
||||
renderer.reset(Rectangle::with_size(size));
|
||||
renderer.draw_geometry(geometry);
|
||||
Headless::screenshot(
|
||||
renderer,
|
||||
Size::new(size.width as u32, size.height as u32),
|
||||
1.,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
fn measure<T>(label: &str, n: usize, mut f: impl FnMut() -> T) {
|
||||
for _ in 0..3 {
|
||||
black_box(f());
|
||||
}
|
||||
let mut values = Vec::new();
|
||||
for _ in 0..n {
|
||||
let now = Instant::now();
|
||||
let value = black_box(f());
|
||||
values.push(now.elapsed().as_secs_f64() * 1000.);
|
||||
drop(value);
|
||||
}
|
||||
values.sort_by(f64::total_cmp);
|
||||
let at = |p: f64| values[((n - 1) as f64 * p).ceil() as usize];
|
||||
println!(
|
||||
"{label}: n={n} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
at(0.5),
|
||||
at(0.95),
|
||||
values[n - 1]
|
||||
);
|
||||
}
|
||||
fn main() {
|
||||
let n = std::env::args()
|
||||
.nth(1)
|
||||
.map(|s| s.parse::<usize>().unwrap())
|
||||
.unwrap_or(20)
|
||||
.max(1);
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut renderer = runtime
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.expect("headless production renderer");
|
||||
println!(
|
||||
"renderer={} samples={n}; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation",
|
||||
renderer.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
] {
|
||||
let pan = Vector::new(-13.25, 9.75);
|
||||
let cache = Cache::new();
|
||||
let calls = Cell::new(0);
|
||||
let cached = |r: &Renderer| {
|
||||
cache.draw(r, size, |f| {
|
||||
calls.set(calls.get() + 1);
|
||||
paint(f, size, zoom, pan);
|
||||
})
|
||||
};
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
println!(
|
||||
"\nsize={}x{} zoom={zoom} dots={}",
|
||||
size.width,
|
||||
size.height,
|
||||
((size.width / step) as i32 + 1) * ((size.height / step) as i32 + 1)
|
||||
);
|
||||
let baseline = fresh(&renderer, size, zoom, pan);
|
||||
let baseline_pixels = screenshot(&mut renderer, size, baseline);
|
||||
let geometry = cached(&renderer);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry));
|
||||
let geometry = cached(&renderer);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry));
|
||||
assert_eq!(calls.get(), 1);
|
||||
println!("pixel_parity=cold_and_warm_exact");
|
||||
measure("fresh_geometry", n, || fresh(&renderer, size, zoom, pan));
|
||||
measure("cached_geometry", n, || cached(&renderer));
|
||||
assert_eq!(calls.get(), 1, "identical camera must reuse geometry");
|
||||
measure("fresh_geometry_and_screenshot", n, || {
|
||||
let g = fresh(&renderer, size, zoom, pan);
|
||||
screenshot(&mut renderer, size, g)
|
||||
});
|
||||
measure("cached_geometry_and_screenshot", n, || {
|
||||
let g = cached(&renderer);
|
||||
screenshot(&mut renderer, size, g)
|
||||
});
|
||||
measure("cache_forced_miss_geometry", n, || {
|
||||
cache.clear();
|
||||
cached(&renderer)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# Independent cache and evaluator review
|
||||
|
||||
Read-only GPT-6 Astra review of
|
||||
`636093aae67aa48fa1d35e403a205227e81d04de`, 2026-09-25. The reviewer did not
|
||||
edit files, compile, launch a desktop window or control the user interface.
|
||||
|
||||
No concrete cache-key, lifetime or drawing-order defect was found. The review
|
||||
checked Iced 0.14 source as well as the application: Iced compares exact cache
|
||||
size, explicit camera changes clear geometry, and context/gesture retirement
|
||||
retains the model-independent grid. Normal canvas state survives view rebuilds;
|
||||
the focus wrapper's suspension reset causes a safe cold miss. Cached and live
|
||||
mesh submissions retain their invocation order. Background geometry has no
|
||||
text/images, and existing model transforms still apply to the live scene.
|
||||
|
||||
The evaluator uses an independent copy of the original grid painter, rather
|
||||
than calling the candidate's painter to produce its expected image. It compares
|
||||
the former combined frame with the split cached background, including opaque
|
||||
and translucent foreground, text, overlay, fractional inputs and live edits.
|
||||
The real DiagramCanvas check demonstrates changing selection/model revision
|
||||
pixels while the grid construction count remains one, then checks pan/resize
|
||||
invalidation. It exercises retained-state drawing and scene invalidation;
|
||||
event-driven native gesture acceptance is outside that test.
|
||||
|
||||
The reviewer inspected `grid-tests-01.log`: all three tests pass, none fail or
|
||||
are ignored, 1,547 are filtered, and execution takes 2.97s. Source identity is
|
||||
provided by the accompanying run record and clean tracked implementation, not
|
||||
by an embedded hash in the Cargo log. Full-suite/build/Clippy results published
|
||||
later remain separate evidence and must be read in their own logs.
|
||||
|
||||
The measurements support avoiding repeated grid construction for an unchanged
|
||||
camera. They do not establish native FPS, performance on another renderer, or
|
||||
improvement during continuously changing pan/zoom. No broad final application
|
||||
or release acceptance is claimed by this review.
|
||||
|
||||
A second read-only audit checked the full desktop log's 1,550 distinct names,
|
||||
1,543 passes and seven ignored cases against `context.json`. It verified all
|
||||
three recorded log hashes, driver-source hash, committed grid blob, linked Iced
|
||||
and Tokio libraries, timing tables, sample method and rejected-batch ranges.
|
||||
It did not independently verify the probe executable, execution chronology,
|
||||
Clippy/build gates or environment assertions. It clarified that run 04's generic
|
||||
cold-and-warm marker is not a production-cache warm-pixel check; that assertion
|
||||
belongs to the separate committed tests.
|
||||
@@ -0,0 +1,209 @@
|
||||
//! Renderer-level phase translation experiment, not application code.
|
||||
use iced::advanced::{Renderer as _, graphics::geometry::Renderer as _, renderer::Headless};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
fn dots(f: &mut Frame, size: Size, step: f32, phase: Vector) {
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
f.fill(
|
||||
&Path::circle(
|
||||
Point::new(phase.x + x as f32 * step, phase.y + y as f32 * step),
|
||||
0.65,
|
||||
),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn old(r: &Renderer, size: Size, step: f32, phase: Vector) -> Geometry {
|
||||
let mut f = Frame::new(r, size);
|
||||
f.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
dots(&mut f, size, step, phase);
|
||||
f.into_geometry()
|
||||
}
|
||||
fn phase(r: &Renderer, cache: &Cache, size: Size, step: f32, builds: &Cell<usize>) -> Geometry {
|
||||
cache.draw_with_bounds(
|
||||
r,
|
||||
Rectangle {
|
||||
x: -step,
|
||||
y: -step,
|
||||
width: size.width + 3. * step,
|
||||
height: size.height + 3. * step,
|
||||
},
|
||||
|f| {
|
||||
builds.set(builds.get() + 1);
|
||||
f.fill_rectangle(
|
||||
Point::new(-step, -step),
|
||||
Size::new(size.width + 3. * step, size.height + 3. * step),
|
||||
BG,
|
||||
);
|
||||
dots(f, size, step, Vector::ZERO);
|
||||
},
|
||||
)
|
||||
}
|
||||
fn submit(r: &mut Renderer, size: Size, g: Geometry, pan: Vector) {
|
||||
r.with_layer(Rectangle::with_size(size), |r| {
|
||||
r.with_translation(pan, |r| r.draw_geometry(g))
|
||||
});
|
||||
}
|
||||
fn shot(r: &mut Renderer, size: Size, scale: f32) -> Vec<u8> {
|
||||
Headless::screenshot(
|
||||
r,
|
||||
Size::new(
|
||||
(size.width * scale).ceil() as u32,
|
||||
(size.height * scale).ceil() as u32,
|
||||
),
|
||||
scale,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
fn main() {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut r = rt
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
println!(
|
||||
"renderer={}; phase-cache prototype; NOT native FPS",
|
||||
r.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
(Size::new(301.5, 211.25), 0.73),
|
||||
] {
|
||||
if size.width != 301.5 {
|
||||
continue;
|
||||
}
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
let cache = Cache::new();
|
||||
let builds = Cell::new(0);
|
||||
println!(
|
||||
"size={}x{} zoom={zoom} step={step}",
|
||||
size.width, size.height
|
||||
);
|
||||
for scale in [1., 1.25, 2.] {
|
||||
for pan in [
|
||||
Vector::ZERO,
|
||||
Vector::new(-13.25, 9.75),
|
||||
Vector::new(6.125, -8.5),
|
||||
Vector::new(step - 0.125, step - 0.25),
|
||||
Vector::new(-0.125, 0.125),
|
||||
] {
|
||||
let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step));
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = old(&r, size, step, offset);
|
||||
r.draw_geometry(g);
|
||||
let expected = shot(&mut r, size, scale);
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = phase(&r, &cache, size, step, &builds);
|
||||
submit(&mut r, size, g, offset);
|
||||
let actual = shot(&mut r, size, scale);
|
||||
assert_eq!(expected.len(), actual.len());
|
||||
for (index, (a, b)) in expected
|
||||
.chunks_exact(4)
|
||||
.zip(actual.chunks_exact(4))
|
||||
.enumerate()
|
||||
.filter(|(_, (a, b))| a != b)
|
||||
.take(20)
|
||||
{
|
||||
println!(
|
||||
"difference_xy={},{} expected={a:?} actual={b:?}",
|
||||
index % (size.width * scale).ceil() as usize,
|
||||
index / (size.width * scale).ceil() as usize
|
||||
);
|
||||
}
|
||||
let changed = expected
|
||||
.chunks_exact(4)
|
||||
.zip(actual.chunks_exact(4))
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
let max = expected
|
||||
.iter()
|
||||
.zip(&actual)
|
||||
.map(|(a, b)| a.abs_diff(*b))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
println!(
|
||||
"scale={scale} pan={},{} changed_pixels={changed} max_channel_delta={max}",
|
||||
pan.x, pan.y
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(builds.get(), 1);
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..23 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset)
|
||||
} else {
|
||||
r.draw_geometry(g)
|
||||
};
|
||||
let elapsed = black_box(now.elapsed().as_secs_f64() * 1000.);
|
||||
if i >= 3 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_and_submit changing_pan n=20 p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
ms[10], ms[19], ms[19]
|
||||
);
|
||||
}
|
||||
println!("cache_builds={}", builds.get());
|
||||
assert_eq!(builds.get(), 1);
|
||||
// Includes backend prepare/upload and readback; still not native presentation.
|
||||
if size.width == 1900. && zoom == 1. || size.width == 1440. && zoom == 0.5 {
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..10 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset);
|
||||
} else {
|
||||
r.draw_geometry(g);
|
||||
}
|
||||
let pixels = black_box(shot(&mut r, size, 1.));
|
||||
let elapsed = now.elapsed().as_secs_f64() * 1000.;
|
||||
drop(pixels);
|
||||
if i >= 2 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_submit_screenshot changing_pan n=8 p50_ms={:.4} min_ms={:.4} max_ms={:.4}",
|
||||
ms[4], ms[0], ms[7]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
//! Renderer-level phase translation experiment, not application code.
|
||||
use iced::advanced::{Renderer as _, graphics::geometry::Renderer as _, renderer::Headless};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
fn dots(f: &mut Frame, size: Size, step: f32, phase: Vector) {
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
f.fill(
|
||||
&Path::circle(
|
||||
Point::new(phase.x + x as f32 * step, phase.y + y as f32 * step),
|
||||
0.65,
|
||||
),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn old(r: &Renderer, size: Size, step: f32, phase: Vector) -> Geometry {
|
||||
let mut f = Frame::new(r, size);
|
||||
f.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
dots(&mut f, size, step, phase);
|
||||
f.into_geometry()
|
||||
}
|
||||
fn phase(r: &Renderer, cache: &Cache, size: Size, step: f32, builds: &Cell<usize>) -> Geometry {
|
||||
cache.draw_with_bounds(
|
||||
r,
|
||||
Rectangle {
|
||||
x: -step,
|
||||
y: -step,
|
||||
width: size.width + 3. * step,
|
||||
height: size.height + 3. * step,
|
||||
},
|
||||
|f| {
|
||||
builds.set(builds.get() + 1);
|
||||
f.fill_rectangle(
|
||||
Point::new(-step, -step),
|
||||
Size::new(size.width + 3. * step, size.height + 3. * step),
|
||||
BG,
|
||||
);
|
||||
dots(f, size, step, Vector::ZERO);
|
||||
},
|
||||
)
|
||||
}
|
||||
fn submit(r: &mut Renderer, size: Size, g: Geometry, pan: Vector) {
|
||||
r.with_layer(Rectangle::with_size(size), |r| {
|
||||
r.with_translation(pan, |r| r.draw_geometry(g))
|
||||
});
|
||||
}
|
||||
fn shot(r: &mut Renderer, size: Size, scale: f32) -> Vec<u8> {
|
||||
Headless::screenshot(
|
||||
r,
|
||||
Size::new(
|
||||
(size.width * scale).ceil() as u32,
|
||||
(size.height * scale).ceil() as u32,
|
||||
),
|
||||
scale,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
fn main() {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut r = rt
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
println!(
|
||||
"renderer={}; phase-cache prototype; NOT native FPS",
|
||||
r.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
(Size::new(301.5, 211.25), 0.73),
|
||||
] {
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
let cache = Cache::new();
|
||||
let builds = Cell::new(0);
|
||||
println!(
|
||||
"size={}x{} zoom={zoom} step={step}",
|
||||
size.width, size.height
|
||||
);
|
||||
for scale in [1., 1.25, 2.] {
|
||||
for pan in [
|
||||
Vector::ZERO,
|
||||
Vector::new(-13.25, 9.75),
|
||||
Vector::new(6.125, -8.5),
|
||||
Vector::new(step - 0.125, step - 0.25),
|
||||
Vector::new(-0.125, 0.125),
|
||||
] {
|
||||
let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step));
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = old(&r, size, step, offset);
|
||||
r.draw_geometry(g);
|
||||
let expected = shot(&mut r, size, scale);
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = phase(&r, &cache, size, step, &builds);
|
||||
submit(&mut r, size, g, offset);
|
||||
let actual = shot(&mut r, size, scale);
|
||||
assert_eq!(expected.len(), actual.len());
|
||||
let changed = expected
|
||||
.chunks_exact(4)
|
||||
.zip(actual.chunks_exact(4))
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
let max = expected
|
||||
.iter()
|
||||
.zip(&actual)
|
||||
.map(|(a, b)| a.abs_diff(*b))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
println!(
|
||||
"scale={scale} pan={},{} changed_pixels={changed} max_channel_delta={max}",
|
||||
pan.x, pan.y
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(builds.get(), 1);
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..23 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset)
|
||||
} else {
|
||||
r.draw_geometry(g)
|
||||
};
|
||||
let elapsed = black_box(now.elapsed().as_secs_f64() * 1000.);
|
||||
if i >= 3 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_and_submit changing_pan n=20 p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
ms[10], ms[19], ms[19]
|
||||
);
|
||||
}
|
||||
println!("cache_builds={}", builds.get());
|
||||
assert_eq!(builds.get(), 1);
|
||||
// Includes backend prepare/upload and readback; still not native presentation.
|
||||
if size.width == 1900. && zoom == 1. || size.width == 1440. && zoom == 0.5 {
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..10 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset);
|
||||
} else {
|
||||
r.draw_geometry(g);
|
||||
}
|
||||
let pixels = black_box(shot(&mut r, size, 1.));
|
||||
let elapsed = now.elapsed().as_secs_f64() * 1000.;
|
||||
drop(pixels);
|
||||
if i >= 2 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_submit_screenshot changing_pan n=8 p50_ms={:.4} min_ms={:.4} max_ms={:.4}",
|
||||
ms[4], ms[0], ms[7]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
//! Renderer-level phase translation experiment, not application code.
|
||||
use iced::advanced::{
|
||||
Renderer as _,
|
||||
graphics::geometry::Renderer as _,
|
||||
renderer::{Headless, Renderer as _},
|
||||
};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
fn dots(f: &mut Frame, size: Size, step: f32, phase: Vector) {
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
f.fill(
|
||||
&Path::circle(
|
||||
Point::new(phase.x + x as f32 * step, phase.y + y as f32 * step),
|
||||
0.65,
|
||||
),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn old(r: &Renderer, size: Size, step: f32, phase: Vector) -> Geometry {
|
||||
let mut f = Frame::new(r, size);
|
||||
f.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
dots(&mut f, size, step, phase);
|
||||
f.into_geometry()
|
||||
}
|
||||
fn phase(r: &Renderer, cache: &Cache, size: Size, step: f32, builds: &Cell<usize>) -> Geometry {
|
||||
cache.draw_with_bounds(
|
||||
r,
|
||||
Rectangle {
|
||||
x: -1.,
|
||||
y: -1.,
|
||||
width: size.width + step + 2.,
|
||||
height: size.height + step + 2.,
|
||||
},
|
||||
|f| {
|
||||
builds.set(builds.get() + 1);
|
||||
dots(f, size, step, Vector::ZERO);
|
||||
},
|
||||
)
|
||||
}
|
||||
fn submit(r: &mut Renderer, size: Size, g: Geometry, pan: Vector) {
|
||||
r.with_layer(Rectangle::with_size(size), |r| {
|
||||
r.with_translation(pan, |r| r.draw_geometry(g))
|
||||
});
|
||||
}
|
||||
fn shot(r: &mut Renderer, size: Size, scale: f32) -> Vec<u8> {
|
||||
Headless::screenshot(
|
||||
r,
|
||||
Size::new(
|
||||
(size.width * scale).ceil() as u32,
|
||||
(size.height * scale).ceil() as u32,
|
||||
),
|
||||
scale,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
fn main() {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut r = rt
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
println!(
|
||||
"renderer={}; phase-cache prototype; NOT native FPS",
|
||||
r.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
(Size::new(301.5, 211.25), 0.73),
|
||||
] {
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
let cache = Cache::new();
|
||||
let builds = Cell::new(0);
|
||||
println!(
|
||||
"size={}x{} zoom={zoom} step={step}",
|
||||
size.width, size.height
|
||||
);
|
||||
for scale in [1., 1.25, 2.] {
|
||||
for pan in [
|
||||
Vector::ZERO,
|
||||
Vector::new(-13.25, 9.75),
|
||||
Vector::new(6.125, -8.5),
|
||||
Vector::new(step - 0.125, step - 0.25),
|
||||
Vector::new(-0.125, 0.125),
|
||||
] {
|
||||
let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step));
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = old(&r, size, step, offset);
|
||||
r.draw_geometry(g);
|
||||
let expected = shot(&mut r, size, scale);
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = phase(&r, &cache, size, step, &builds);
|
||||
submit(&mut r, size, g, offset);
|
||||
let actual = shot(&mut r, size, scale);
|
||||
assert_eq!(expected.len(), actual.len());
|
||||
let changed = expected
|
||||
.chunks_exact(4)
|
||||
.zip(actual.chunks_exact(4))
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
let max = expected
|
||||
.iter()
|
||||
.zip(&actual)
|
||||
.map(|(a, b)| a.abs_diff(*b))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
println!(
|
||||
"scale={scale} pan={},{} changed_pixels={changed} max_channel_delta={max}",
|
||||
pan.x, pan.y
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(builds.get(), 1);
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..23 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset)
|
||||
} else {
|
||||
r.draw_geometry(g)
|
||||
};
|
||||
let elapsed = black_box(now.elapsed().as_secs_f64() * 1000.);
|
||||
if i >= 3 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_and_submit changing_pan n=20 p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
ms[10], ms[19], ms[19]
|
||||
);
|
||||
}
|
||||
println!("cache_builds={}", builds.get());
|
||||
assert_eq!(builds.get(), 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
//! Full-sequence opaque phase replay with diagnostics and repeat controls; not application code.
|
||||
use iced::advanced::{Renderer as _, graphics::geometry::Renderer as _, renderer::Headless};
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
fn dots(f: &mut Frame, size: Size, step: f32, phase: Vector) {
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
f.fill(
|
||||
&Path::circle(
|
||||
Point::new(phase.x + x as f32 * step, phase.y + y as f32 * step),
|
||||
0.65,
|
||||
),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn old(r: &Renderer, size: Size, step: f32, phase: Vector) -> Geometry {
|
||||
let mut f = Frame::new(r, size);
|
||||
f.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
dots(&mut f, size, step, phase);
|
||||
f.into_geometry()
|
||||
}
|
||||
fn phase(r: &Renderer, cache: &Cache, size: Size, step: f32, builds: &Cell<usize>) -> Geometry {
|
||||
cache.draw_with_bounds(
|
||||
r,
|
||||
Rectangle {
|
||||
x: -step,
|
||||
y: -step,
|
||||
width: size.width + 3. * step,
|
||||
height: size.height + 3. * step,
|
||||
},
|
||||
|f| {
|
||||
builds.set(builds.get() + 1);
|
||||
f.fill_rectangle(
|
||||
Point::new(-step, -step),
|
||||
Size::new(size.width + 3. * step, size.height + 3. * step),
|
||||
BG,
|
||||
);
|
||||
dots(f, size, step, Vector::ZERO);
|
||||
},
|
||||
)
|
||||
}
|
||||
fn submit(r: &mut Renderer, size: Size, g: Geometry, pan: Vector) {
|
||||
r.with_layer(Rectangle::with_size(size), |r| {
|
||||
r.with_translation(pan, |r| r.draw_geometry(g))
|
||||
});
|
||||
}
|
||||
fn shot(r: &mut Renderer, size: Size, scale: f32) -> Vec<u8> {
|
||||
Headless::screenshot(
|
||||
r,
|
||||
Size::new(
|
||||
(size.width * scale).ceil() as u32,
|
||||
(size.height * scale).ceil() as u32,
|
||||
),
|
||||
scale,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
// Diagnostics only: this performs no rendering and does not mutate the renderer.
|
||||
fn log_coordinates(expected: &[u8], actual: &[u8], size: Size, scale: f32) {
|
||||
assert_eq!(expected.len(), actual.len());
|
||||
let physical_width = (size.width * scale).ceil() as usize;
|
||||
for (index, (a, b)) in expected
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.zip(actual.as_chunks::<4>().0)
|
||||
.enumerate()
|
||||
.filter(|(_, (a, b))| a != b)
|
||||
.take(20)
|
||||
{
|
||||
println!(
|
||||
"difference_xy={},{} expected={a:?} actual={b:?}",
|
||||
index % physical_width,
|
||||
index / physical_width
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn report_control(label: &str, expected: &[u8], actual: &[u8], size: Size, scale: f32) {
|
||||
assert_eq!(expected.len(), actual.len());
|
||||
let changed = expected
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.zip(actual.as_chunks::<4>().0)
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
let max = expected
|
||||
.iter()
|
||||
.zip(actual)
|
||||
.map(|(a, b)| a.abs_diff(*b))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
println!(
|
||||
"comparison={label} size={}x{} scale={scale} pan=-13.25,9.75 changed_pixels={changed} max_channel_delta={max}",
|
||||
size.width, size.height
|
||||
);
|
||||
log_coordinates(expected, actual, size, scale);
|
||||
}
|
||||
fn main() {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut r = rt
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
println!(
|
||||
"renderer={}; phase-cache prototype; NOT native FPS",
|
||||
r.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
(Size::new(301.5, 211.25), 0.73),
|
||||
] {
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
let cache = Cache::new();
|
||||
let builds = Cell::new(0);
|
||||
println!(
|
||||
"size={}x{} zoom={zoom} step={step}",
|
||||
size.width, size.height
|
||||
);
|
||||
for scale in [1., 1.25, 2.] {
|
||||
for pan in [
|
||||
Vector::ZERO,
|
||||
Vector::new(-13.25, 9.75),
|
||||
Vector::new(6.125, -8.5),
|
||||
Vector::new(step - 0.125, step - 0.25),
|
||||
Vector::new(-0.125, 0.125),
|
||||
] {
|
||||
let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step));
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = old(&r, size, step, offset);
|
||||
r.draw_geometry(g);
|
||||
let expected = shot(&mut r, size, scale);
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = phase(&r, &cache, size, step, &builds);
|
||||
submit(&mut r, size, g, offset);
|
||||
let actual = shot(&mut r, size, scale);
|
||||
assert_eq!(expected.len(), actual.len());
|
||||
let changed = expected
|
||||
.chunks_exact(4)
|
||||
.zip(actual.chunks_exact(4))
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
let max = expected
|
||||
.iter()
|
||||
.zip(&actual)
|
||||
.map(|(a, b)| a.abs_diff(*b))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
println!(
|
||||
"scale={scale} pan={},{} changed_pixels={changed} max_channel_delta={max}",
|
||||
pan.x, pan.y
|
||||
);
|
||||
// Keep the original render/timing sequence; print diagnostics only.
|
||||
log_coordinates(&expected, &actual, size, scale);
|
||||
}
|
||||
}
|
||||
assert_eq!(builds.get(), 1);
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..23 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset)
|
||||
} else {
|
||||
r.draw_geometry(g)
|
||||
};
|
||||
let elapsed = black_box(now.elapsed().as_secs_f64() * 1000.);
|
||||
if i >= 3 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_and_submit changing_pan n=20 p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
ms[10], ms[19], ms[19]
|
||||
);
|
||||
}
|
||||
println!("cache_builds={}", builds.get());
|
||||
assert_eq!(builds.get(), 1);
|
||||
// Includes backend prepare/upload and readback; still not native presentation.
|
||||
if size.width == 1900. && zoom == 1. || size.width == 1440. && zoom == 0.5 {
|
||||
for cached in [false, true] {
|
||||
let mut ms = Vec::new();
|
||||
for i in 0..10 {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let offset = Vector::new((i as f32 * 0.25).rem_euclid(step), 9.75);
|
||||
let now = Instant::now();
|
||||
let g = if cached {
|
||||
phase(&r, &cache, size, step, &builds)
|
||||
} else {
|
||||
old(&r, size, step, offset)
|
||||
};
|
||||
if cached {
|
||||
submit(&mut r, size, g, offset);
|
||||
} else {
|
||||
r.draw_geometry(g);
|
||||
}
|
||||
let pixels = black_box(shot(&mut r, size, 1.));
|
||||
let elapsed = now.elapsed().as_secs_f64() * 1000.;
|
||||
drop(pixels);
|
||||
if i >= 2 {
|
||||
ms.push(elapsed);
|
||||
}
|
||||
}
|
||||
ms.sort_by(f64::total_cmp);
|
||||
println!(
|
||||
"cached={cached} construct_submit_screenshot changing_pan n=8 p50_ms={:.4} min_ms={:.4} max_ms={:.4}",
|
||||
ms[4], ms[0], ms[7]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// All original cases, timings and screenshots have completed above.
|
||||
// Reuse the same renderer for controls. This cache is newly constructed,
|
||||
// then retained across all control repetitions; original per-case caches
|
||||
// have already gone out of scope, exactly as in the opaque probe.
|
||||
println!("original_sequence_complete; beginning same-renderer repeat controls");
|
||||
let size = Size::new(301.5, 211.25);
|
||||
let step = (24f32 * 0.73).max(14.);
|
||||
let pan: Vector<f32> = Vector::new(-13.25, 9.75);
|
||||
let offset = Vector::new(pan.x.rem_euclid(step), pan.y.rem_euclid(step));
|
||||
let cache = Cache::new();
|
||||
let builds = Cell::new(0);
|
||||
for repetition in 1..=3 {
|
||||
for scale in [1., 2.] {
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = old(&r, size, step, offset);
|
||||
r.draw_geometry(g);
|
||||
let first = shot(&mut r, size, scale);
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = old(&r, size, step, offset);
|
||||
r.draw_geometry(g);
|
||||
let second = shot(&mut r, size, scale);
|
||||
report_control(
|
||||
&format!("old/old repetition={repetition}"),
|
||||
&first,
|
||||
&second,
|
||||
size,
|
||||
scale,
|
||||
);
|
||||
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = phase(&r, &cache, size, step, &builds);
|
||||
submit(&mut r, size, g, offset);
|
||||
let first = shot(&mut r, size, scale);
|
||||
r.reset(Rectangle::with_size(size));
|
||||
let g = phase(&r, &cache, size, step, &builds);
|
||||
submit(&mut r, size, g, offset);
|
||||
let second = shot(&mut r, size, scale);
|
||||
report_control(
|
||||
&format!("cached/cached repetition={repetition}"),
|
||||
&first,
|
||||
&second,
|
||||
size,
|
||||
scale,
|
||||
);
|
||||
}
|
||||
}
|
||||
println!("control_cache_builds={}", builds.get());
|
||||
assert_eq!(builds.get(), 1);
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//! Headless rendering experiment; creates no desktop window and sends no input.
|
||||
use iced::advanced::{
|
||||
graphics::geometry::Renderer as _,
|
||||
renderer::{Headless, Renderer as _},
|
||||
};
|
||||
use iced::widget::canvas;
|
||||
use iced::{
|
||||
Color, Point, Rectangle, Renderer, Size, Vector,
|
||||
widget::canvas::{Cache, Frame, Geometry, Path},
|
||||
};
|
||||
use std::{cell::Cell, hint::black_box, time::Instant};
|
||||
|
||||
// Compile the actual candidate cache, not a second implementation of its key.
|
||||
#[path = "../../../crates/oa-desktop/src/diagram/grid.rs"]
|
||||
mod grid;
|
||||
mod style {
|
||||
pub const LINE: iced::Color = iced::Color::from_rgb(0.86, 0.89, 0.91);
|
||||
}
|
||||
|
||||
const BG: Color = Color::from_rgb8(246, 248, 251);
|
||||
const DOT: Color = Color::from_rgb(0.86, 0.89, 0.91);
|
||||
|
||||
fn paint(frame: &mut Frame, size: Size, zoom: f32, pan: Vector) {
|
||||
frame.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
frame.fill(
|
||||
&Path::circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65),
|
||||
DOT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn fresh(renderer: &Renderer, size: Size, zoom: f32, pan: Vector) -> Geometry {
|
||||
let mut frame = Frame::new(renderer, size);
|
||||
paint(&mut frame, size, zoom, pan);
|
||||
frame.into_geometry()
|
||||
}
|
||||
fn batched(renderer: &Renderer, size: Size, zoom: f32, pan: Vector) -> Geometry {
|
||||
let mut frame = Frame::new(renderer, size);
|
||||
frame.fill_rectangle(Point::ORIGIN, size, BG);
|
||||
let step = (24. * zoom).max(14.);
|
||||
let ox = pan.x.rem_euclid(step);
|
||||
let oy = pan.y.rem_euclid(step);
|
||||
let path = Path::new(|p| {
|
||||
for x in 0..(size.width / step) as i32 + 1 {
|
||||
for y in 0..(size.height / step) as i32 + 1 {
|
||||
p.circle(Point::new(ox + x as f32 * step, oy + y as f32 * step), 0.65);
|
||||
p.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
frame.fill(&path, DOT);
|
||||
frame.into_geometry()
|
||||
}
|
||||
fn screenshot(renderer: &mut Renderer, size: Size, geometry: Geometry) -> Vec<u8> {
|
||||
renderer.reset(Rectangle::with_size(size));
|
||||
renderer.draw_geometry(geometry);
|
||||
Headless::screenshot(
|
||||
renderer,
|
||||
Size::new(size.width as u32, size.height as u32),
|
||||
1.,
|
||||
BG,
|
||||
)
|
||||
}
|
||||
fn measure<T>(label: &str, n: usize, mut f: impl FnMut() -> T) {
|
||||
for _ in 0..3 {
|
||||
black_box(f());
|
||||
}
|
||||
let mut values = Vec::new();
|
||||
for _ in 0..n {
|
||||
let now = Instant::now();
|
||||
let value = black_box(f());
|
||||
values.push(now.elapsed().as_secs_f64() * 1000.);
|
||||
drop(value);
|
||||
}
|
||||
values.sort_by(f64::total_cmp);
|
||||
let at = |p: f64| values[((n - 1) as f64 * p).ceil() as usize];
|
||||
println!(
|
||||
"{label}: n={n} p50_ms={:.4} p95_ms={:.4} max_ms={:.4}",
|
||||
at(0.5),
|
||||
at(0.95),
|
||||
values[n - 1]
|
||||
);
|
||||
}
|
||||
fn main() {
|
||||
let n = std::env::args()
|
||||
.nth(1)
|
||||
.map(|s| s.parse::<usize>().unwrap())
|
||||
.unwrap_or(20)
|
||||
.max(1);
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut renderer = runtime
|
||||
.block_on(<Renderer as Headless>::new(
|
||||
iced::Font::DEFAULT,
|
||||
16.into(),
|
||||
None,
|
||||
))
|
||||
.expect("headless production renderer");
|
||||
println!(
|
||||
"renderer={} samples={n}; screenshot includes offscreen rendering + GPU readback, NOT native frame presentation",
|
||||
renderer.name()
|
||||
);
|
||||
for (size, zoom) in [
|
||||
(Size::new(1900., 660.), 1.),
|
||||
(Size::new(1900., 660.), 0.5),
|
||||
(Size::new(1440., 900.), 1.),
|
||||
(Size::new(1440., 900.), 0.5),
|
||||
] {
|
||||
let pan = Vector::new(-13.25, 9.75);
|
||||
let cache = Cache::new();
|
||||
let calls = Cell::new(0);
|
||||
let cached = |r: &Renderer| {
|
||||
cache.draw(r, size, |f| {
|
||||
calls.set(calls.get() + 1);
|
||||
paint(f, size, zoom, pan);
|
||||
})
|
||||
};
|
||||
let step = (24f32 * zoom).max(14.);
|
||||
println!(
|
||||
"\nsize={}x{} zoom={zoom} dots={}",
|
||||
size.width,
|
||||
size.height,
|
||||
((size.width / step) as i32 + 1) * ((size.height / step) as i32 + 1)
|
||||
);
|
||||
let baseline = fresh(&renderer, size, zoom, pan);
|
||||
let baseline_pixels = screenshot(&mut renderer, size, baseline);
|
||||
let geometry = batched(&renderer, size, zoom, pan);
|
||||
let pixels = screenshot(&mut renderer, size, geometry);
|
||||
let differences = baseline_pixels
|
||||
.iter()
|
||||
.zip(&pixels)
|
||||
.filter(|(a, b)| a != b)
|
||||
.count();
|
||||
println!("batched_rgba_byte_differences={differences}");
|
||||
assert_eq!(
|
||||
differences, 0,
|
||||
"batched path must preserve exact grid pixels"
|
||||
);
|
||||
let geometry = cached(&renderer);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry));
|
||||
let geometry = cached(&renderer);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, geometry));
|
||||
assert_eq!(calls.get(), 1);
|
||||
println!("pixel_parity=cold_and_warm_exact");
|
||||
measure("fresh_geometry", n, || fresh(&renderer, size, zoom, pan));
|
||||
let production = grid::Grid::default();
|
||||
let g = production.draw(&renderer, size, zoom, pan);
|
||||
assert_eq!(baseline_pixels, screenshot(&mut renderer, size, g));
|
||||
measure("production_cached_geometry", n, || {
|
||||
production.draw(&renderer, size, zoom, pan)
|
||||
});
|
||||
let mut tick = 0;
|
||||
measure("production_changing_pan_geometry", n, || {
|
||||
tick += 1;
|
||||
production.draw(
|
||||
&renderer,
|
||||
size,
|
||||
zoom,
|
||||
pan + Vector::new(tick as f32 * 0.25, 0.),
|
||||
)
|
||||
});
|
||||
measure("cached_geometry", n, || cached(&renderer));
|
||||
assert_eq!(calls.get(), 1, "identical camera must reuse geometry");
|
||||
measure("fresh_geometry_and_screenshot", n, || {
|
||||
let g = fresh(&renderer, size, zoom, pan);
|
||||
screenshot(&mut renderer, size, g)
|
||||
});
|
||||
measure("cached_geometry_and_screenshot", n, || {
|
||||
let g = cached(&renderer);
|
||||
screenshot(&mut renderer, size, g)
|
||||
});
|
||||
measure("cache_forced_miss_geometry", n, || {
|
||||
cache.clear();
|
||||
cached(&renderer)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,30 @@ actionable defect in the stated boundary. These do not substitute for the final
|
||||
combined execution. The vendor patch-description correction is incorporated at
|
||||
`17eaa03fd69741dff746f21d150dc04c341411b3`.
|
||||
|
||||
## Mounted State picker correction
|
||||
|
||||
The combined Windows test run at `8ddfadf440b5c208b1adb6b211a97ba0c99cff04`
|
||||
failed the mounted State missing-picker Escape regression. The test located the
|
||||
picker relative to the canvas, but the integrated selection toolbar adds 56px
|
||||
plus an 8px gap between them. Both the opening and option clicks were stale.
|
||||
Parent and independent source inspection agreed on this fixture defect; they
|
||||
found no reason to change the production Escape policy from this failure.
|
||||
|
||||
Successor `a9cd364eacbbffa0693bab05b5a2b65f9d105535` adds a zero-padding
|
||||
identified container around the existing fixed-height picker slot. The fixture
|
||||
measures that slot, checks Escape capture before App subscriptions, and retains
|
||||
zero messages, exact document/source/selection/camera/revision/history,
|
||||
reopen-and-Place, and exact source Undo checks. The option coordinate assumes
|
||||
this fixture's single downward-opening menu at 1800 by 1100; it is not a general
|
||||
overlay geometry evaluator. No production Escape handler changes.
|
||||
|
||||
The peer records 2,199 passes, one failure and eight ignores for that failed
|
||||
full run, with lint and product builds not run. The earlier final-02 attempt
|
||||
was interrupted during harness listing before test bodies; neither attempt is
|
||||
a passed gate. [Preserved attempts and v2 recipe](http://217.177.34.108:3000/sorokin.oleg.edu/ARCADIA-Studio/src/commit/184c1307d1ae3d49007480020fa1c080c94ececa/harness/prerelease37-windows-gates-20260924/attempts-8dd)
|
||||
retain their distinct boundaries. The corrected source requires current
|
||||
combined Windows gates and packaged native acceptance.
|
||||
|
||||
## Combined gates and package handoff
|
||||
|
||||
The first peer compilation exposed E0425 in the new Route fixture: its
|
||||
|
||||
Reference in New Issue
Block a user