Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ doctest = false

[[bench]]
name = "stack"
harness = false

[[bench]]
name = "layout_modes"
harness = false
214 changes: 214 additions & 0 deletions benches/layout_modes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput};
use morphorm::*;
use morphorm_ecs::*;

fn root(world: &mut World, layout_type: LayoutType, width: f32, height: f32) -> Entity {
let root = world.add(None);
world.set_layout_type(root, layout_type);
world.set_width(root, Units::Pixels(width));
world.set_height(root, Units::Pixels(height));
world.set_alignment(root, Alignment::TopLeft);
root
}

fn build_stack(world: &mut World, parent: Entity, layout_type: LayoutType, breadth: usize, depth: usize) {
if depth == 0 {
return;
}

for index in 0..breadth {
let child = world.add(Some(parent));
world.set_layout_type(child, layout_type);
world.set_width(child, Units::Stretch(1.0));
world.set_height(child, if index % 3 == 0 { Units::Pixels(24.0) } else { Units::Stretch(1.0) });
build_stack(world, child, layout_type, breadth, depth - 1);
}
}

fn stack_world(layout_type: LayoutType) -> (World, Entity) {
let mut world = World::default();
let root = root(&mut world, layout_type, 1200.0, 900.0);
build_stack(&mut world, root, layout_type, 8, 3);
(world, root)
}

fn wrap_world(layout_type: LayoutType) -> (World, Entity) {
let mut world = World::default();
let root = root(&mut world, layout_type, 800.0, 600.0);
world.set_wrap(root, LayoutWrap::Wrap);
world.set_horizontal_gap(root, Units::Pixels(4.0));
world.set_vertical_gap(root, Units::Pixels(4.0));

for index in 0..256 {
let child = world.add(Some(root));
world.set_width(child, if index % 4 == 0 { Units::Stretch(1.0) } else { Units::Pixels(48.0) });
world.set_height(child, if index % 5 == 0 { Units::Stretch(1.0) } else { Units::Pixels(24.0) });
world.set_min_width(child, Units::Pixels(20.0));
world.set_min_height(child, Units::Pixels(16.0));
}

(world, root)
}

fn overlay_world(auto_size: bool) -> (World, Entity) {
let mut world = World::default();
let root = root(&mut world, LayoutType::Overlay, 800.0, 600.0);
world.set_alignment(root, Alignment::Center);

if auto_size {
world.set_width(root, Units::Auto);
world.set_height(root, Units::Auto);
world.set_min_width(root, Units::Auto);
world.set_min_height(root, Units::Auto);
}

for index in 0..256 {
let child = world.add(Some(root));
world.set_width(child, Units::Pixels(32.0 + (index % 8) as f32 * 4.0));
world.set_height(child, Units::Pixels(20.0 + (index % 5) as f32 * 3.0));
}

(world, root)
}

fn grid_world(auto_tracks: bool) -> (World, Entity) {
const SIDE: usize = 16;

let mut world = World::default();
let root = root(&mut world, LayoutType::Grid, 1024.0, 768.0);
let track = if auto_tracks { Units::Auto } else { Units::Stretch(1.0) };
world.set_grid_columns(root, vec![track; SIDE]);
world.set_grid_rows(root, vec![track; SIDE]);
world.set_horizontal_gap(root, Units::Pixels(2.0));
world.set_vertical_gap(root, Units::Pixels(2.0));

for row in 0..SIDE {
for column in 0..SIDE {
let child = world.add(Some(root));
world.set_column_start(child, column);
world.set_row_start(child, row);
world.set_width(child, Units::Pixels(24.0 + (column % 4) as f32 * 4.0));
world.set_height(child, Units::Pixels(18.0 + (row % 3) as f32 * 3.0));
}
}

(world, root)
}

fn incremental_world(auto_parent: bool) -> (World, Entity, Entity) {
let mut world = World::default();
let root = root(&mut world, LayoutType::Column, 1200.0, 900.0);
let mut dirty = root;

for branch in 0..8 {
let parent = world.add(Some(root));
world.set_layout_type(parent, LayoutType::Column);
world.set_width(parent, Units::Pixels(120.0));
world.set_height(parent, if auto_parent && branch == 0 { Units::Auto } else { Units::Pixels(100.0) });

for child_index in 0..64 {
let child = world.add(Some(parent));
world.set_width(child, Units::Pixels(20.0));
world.set_height(child, Units::Pixels(12.0));
if branch == 0 && child_index == 0 {
dirty = child;
}
}
}

root.layout(&mut world.cache, &world.tree, &world.store, &mut ());
world.set_height(dirty, Units::Pixels(16.0));
(world, root, dirty)
}

fn benchmark_full_layout(c: &mut Criterion) {
let mut group = c.benchmark_group("layout modes");
group.sample_size(20);

for layout_type in [LayoutType::Row, LayoutType::Column] {
group.throughput(Throughput::Elements(585));
group.bench_with_input(
BenchmarkId::new("stack", format!("{layout_type:?}")),
&layout_type,
|b, &layout_type| {
b.iter_batched(
|| stack_world(layout_type),
|(mut world, root)| root.layout(&mut world.cache, &world.tree, &world.store, &mut ()),
BatchSize::SmallInput,
);
},
);
}

for layout_type in [LayoutType::Row, LayoutType::Column] {
group.throughput(Throughput::Elements(257));
group.bench_with_input(
BenchmarkId::new("wrap", format!("{layout_type:?}")),
&layout_type,
|b, &layout_type| {
b.iter_batched(
|| wrap_world(layout_type),
|(mut world, root)| root.layout(&mut world.cache, &world.tree, &world.store, &mut ()),
BatchSize::SmallInput,
);
},
);
}

for auto_size in [false, true] {
group.throughput(Throughput::Elements(257));
group.bench_with_input(
BenchmarkId::new("overlay", if auto_size { "auto" } else { "fixed" }),
&auto_size,
|b, &auto_size| {
b.iter_batched(
|| overlay_world(auto_size),
|(mut world, root)| root.layout(&mut world.cache, &world.tree, &world.store, &mut ()),
BatchSize::SmallInput,
);
},
);
}

for auto_tracks in [false, true] {
group.throughput(Throughput::Elements(257));
group.bench_with_input(
BenchmarkId::new("grid", if auto_tracks { "auto" } else { "stretch" }),
&auto_tracks,
|b, &auto_tracks| {
b.iter_batched(
|| grid_world(auto_tracks),
|(mut world, root)| root.layout(&mut world.cache, &world.tree, &world.store, &mut ()),
BatchSize::SmallInput,
);
},
);
}

group.finish();
}

fn benchmark_incremental_layout(c: &mut Criterion) {
let mut group = c.benchmark_group("incremental layout");
group.sample_size(20);
group.throughput(Throughput::Elements(521));

for auto_parent in [false, true] {
group.bench_with_input(
BenchmarkId::new("dirty leaf", if auto_parent { "auto parent" } else { "fixed parent" }),
&auto_parent,
|b, &auto_parent| {
b.iter_batched(
|| incremental_world(auto_parent),
|(mut world, _root, dirty)| dirty.layout(&mut world.cache, &world.tree, &world.store, &mut ()),
BatchSize::SmallInput,
);
},
);
}

group.finish();
}

criterion_group!(benches, benchmark_full_layout, benchmark_incremental_layout);
criterion_main!(benches);
17 changes: 16 additions & 1 deletion ecs/src/implementations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ pub struct Rect {
pub struct NodeCache {
// Computed size and position of nodes.
pub rect: SecondaryMap<Entity, Rect>,
pub layout: SecondaryMap<Entity, CachedLayout>,
}

impl NodeCache {
Expand All @@ -190,10 +191,12 @@ impl NodeCache {

pub fn remove(&mut self, entity: Entity) {
self.rect.remove(entity);
self.layout.remove(entity);
}

pub fn clear(&mut self) {
self.rect.clear();
self.layout.clear();
}

pub fn bounds(&self, entity: Entity) -> Option<&Rect> {
Expand Down Expand Up @@ -244,10 +247,22 @@ impl Cache for NodeCache {

0.0
}

fn cached_layout(&self, node: &Self::Node) -> Option<CachedLayout> {
self.layout.get(*node).copied()
}

fn set_cached_layout(&mut self, node: &Self::Node, layout: CachedLayout) {
self.layout.insert(*node, layout);
}

fn invalidate_cached_layout(&mut self, node: &Self::Node) {
self.layout.remove(*node);
}
}

impl Default for NodeCache {
fn default() -> Self {
Self { rect: SecondaryMap::new() }
Self { rect: SecondaryMap::new(), layout: SecondaryMap::new() }
}
}
21 changes: 21 additions & 0 deletions src/cache.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
use crate::{LayoutType, Node};

/// The inputs and result of a completed node layout, used to reuse unchanged subtrees.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CachedLayout {
pub parent_layout_type: LayoutType,
pub parent_main: f32,
pub parent_cross: f32,
pub main: f32,
pub cross: f32,
}

/// The `Cache` is a store which contains the computed size and position of nodes
/// after a layout calculation.
///
Expand All @@ -20,6 +30,17 @@ pub trait Cache {

/// Sets the cached position and size of the given node.
fn set_bounds(&mut self, node: &Self::Node, posx: f32, posy: f32, width: f32, height: f32);

/// Returns the inputs and result of the node's most recent valid layout.
fn cached_layout(&self, _node: &Self::Node) -> Option<CachedLayout> {
None
}

/// Stores the inputs and result of a completed node layout.
fn set_cached_layout(&mut self, _node: &Self::Node, _layout: CachedLayout) {}

/// Invalidates a previously cached node layout.
fn invalidate_cached_layout(&mut self, _node: &Self::Node) {}
}

/// Helper trait for getting/setting node position/size in a direction agnostic way.
Expand Down
Loading
Loading