Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tree arena #752

Merged
merged 16 commits into from
Dec 5, 2024
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ members = [
"xilem_web/web_examples/spawn_tasks",
"xilem_web/web_examples/svgtoy",
"xilem_web/web_examples/svgdraw",
"tree_arena",
]

[workspace.package]
Expand Down Expand Up @@ -105,6 +106,7 @@ clippy.duplicated_attributes = "allow"
[workspace.dependencies]
masonry = { version = "0.2.0", path = "masonry" }
xilem_core = { version = "0.1.0", path = "xilem_core" }
tree_arena = { version = "0.1.0", path = "tree_arena" }
vello = "0.3"
wgpu = "22.1.0"
kurbo = "0.11.1"
Expand Down
1 change: 1 addition & 0 deletions masonry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ vello.workspace = true
wgpu.workspace = true
parley.workspace = true
winit.workspace = true
tree_arena.workspace = true
smallvec.workspace = true
tracing = { workspace = true, features = ["default"] }
image.workspace = true
Expand Down
2 changes: 1 addition & 1 deletion masonry/src/contexts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use accesskit::TreeUpdate;
use dpi::LogicalPosition;
use parley::{FontContext, LayoutContext};
use tracing::{trace, warn};
use tree_arena::{ArenaMutChildren, ArenaRefChildren};
use vello::kurbo::Vec2;
use vello::peniko::Color;
use winit::window::ResizeDirection;
Expand All @@ -18,7 +19,6 @@ use crate::passes::layout::run_layout_on;
use crate::render_root::{MutateCallback, RenderRootSignal, RenderRootState};
use crate::text::BrushIndex;
use crate::theme::get_debug_color;
use crate::tree_arena::{ArenaMutChildren, ArenaRefChildren};
use crate::widget::{WidgetMut, WidgetRef, WidgetState};
use crate::{AllowRawMut, BoxConstraints, Insets, Point, Rect, Size, Widget, WidgetId, WidgetPod};

Expand Down
1 change: 0 additions & 1 deletion masonry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ mod paint_scene_helpers;
mod passes;
mod render_root;
mod tracing_backend;
mod tree_arena;

pub mod event_loop_runner;
pub mod testing;
Expand Down
2 changes: 1 addition & 1 deletion masonry/src/passes/accessibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

use accesskit::{Node, NodeId, Tree, TreeUpdate};
use tracing::{debug, info_span, trace};
use tree_arena::ArenaMut;
use vello::kurbo::Rect;

use crate::passes::recurse_on_children;
use crate::render_root::{RenderRoot, RenderRootState};
use crate::tree_arena::ArenaMut;
use crate::{AccessCtx, Widget, WidgetState};

use super::enter_span_if;
Expand Down
2 changes: 1 addition & 1 deletion masonry/src/passes/anim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// SPDX-License-Identifier: Apache-2.0

use tracing::info_span;
use tree_arena::ArenaMut;

use crate::passes::{enter_span_if, recurse_on_children};
use crate::render_root::{RenderRoot, RenderRootState};
use crate::tree_arena::ArenaMut;
use crate::{UpdateCtx, Widget, WidgetState};

// --- MARK: UPDATE ANIM ---
Expand Down
2 changes: 1 addition & 1 deletion masonry/src/passes/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
// SPDX-License-Identifier: Apache-2.0

use tracing::info_span;
use tree_arena::ArenaMut;
use vello::kurbo::Vec2;

use crate::passes::{enter_span_if, recurse_on_children};
use crate::render_root::{RenderRoot, RenderRootSignal, RenderRootState};
use crate::tree_arena::ArenaMut;
use crate::{ComposeCtx, Widget, WidgetState};

// --- MARK: RECURSE ---
Expand Down
2 changes: 1 addition & 1 deletion masonry/src/passes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

use tracing::span::EnteredSpan;
use tree_arena::{ArenaMut, ArenaMutChildren, ArenaRef};

use crate::render_root::RenderRootState;
use crate::tree_arena::{ArenaMut, ArenaMutChildren, ArenaRef};
use crate::widget::WidgetArena;
use crate::{QueryCtx, Widget, WidgetId, WidgetState};

Expand Down
2 changes: 1 addition & 1 deletion masonry/src/passes/paint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
use std::collections::HashMap;

use tracing::{info_span, trace};
use tree_arena::ArenaMut;
use vello::kurbo::{Affine, Stroke};
use vello::peniko::Mix;
use vello::Scene;

use crate::passes::{enter_span_if, recurse_on_children};
use crate::render_root::{RenderRoot, RenderRootState};
use crate::theme::get_debug_color;
use crate::tree_arena::ArenaMut;
use crate::{PaintCtx, Widget, WidgetId, WidgetState};

// --- MARK: PAINT WIDGET ---
Expand Down
2 changes: 1 addition & 1 deletion masonry/src/passes/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ use std::collections::HashSet;

use cursor_icon::CursorIcon;
use tracing::{info_span, trace};
use tree_arena::ArenaMut;

use crate::passes::event::{run_on_pointer_event_pass, run_on_text_event_pass};
use crate::passes::{enter_span, enter_span_if, merge_state_up, recurse_on_children};
use crate::render_root::{RenderRoot, RenderRootSignal, RenderRootState};
use crate::tree_arena::ArenaMut;
use crate::{
PointerEvent, QueryCtx, RegisterCtx, TextEvent, Update, UpdateCtx, Widget, WidgetId,
WidgetState,
Expand Down
2 changes: 1 addition & 1 deletion masonry/src/render_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use accesskit::{ActionRequest, TreeUpdate};
use parley::fontique::{self, Collection, CollectionOptions};
use parley::{FontContext, LayoutContext};
use tracing::{info_span, warn};
use tree_arena::{ArenaMut, TreeArena};
use vello::kurbo::{self, Rect};
use vello::Scene;
use winit::window::ResizeDirection;
Expand Down Expand Up @@ -35,7 +36,6 @@ use crate::passes::update::{
};
use crate::passes::{recurse_on_children, PassTracing};
use crate::text::BrushIndex;
use crate::tree_arena::{ArenaMut, TreeArena};
use crate::widget::{WidgetArena, WidgetMut, WidgetRef, WidgetState};
use crate::{AccessEvent, Action, CursorIcon, Handled, QueryCtx, Widget, WidgetId, WidgetPod};

Expand Down
3 changes: 2 additions & 1 deletion masonry/src/widget/widget_arena.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0

use crate::tree_arena::{ArenaMut, ArenaRef, TreeArena};
use tree_arena::{ArenaMut, ArenaRef, TreeArena};

use crate::{Widget, WidgetId, WidgetState};

pub(crate) struct WidgetArena {
Expand Down
34 changes: 34 additions & 0 deletions tree_arena/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[package]
name = "tree_arena"
version = "0.1.0"
description = "An arena allocated tree designed for Linebender"
keywords = ["arena", "tree"]
categories = ["gui"]
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true


[package.metadata.docs.rs]
all-features = true # all features are enabled - this means that the generated docs are for the safe version
# There are no platform specific docs.
default-target = "x86_64-unknown-linux-gnu"
targets = []
# rustdoc-scrape-examples tracking issue https://github.com/rust-lang/rust/issues/88791
# cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] # no examples yet


[dependencies]

[features]
# This crate contains two implementations of a tree for use in masonry, one safe and the other unsafe.
# The safe tree is known to work, and serves as the baseline implementation and is used by default.
# The unsafe tree leverages a hashmap as an arena and is designed for higher performance: it leverages unsafe code to achieve this.
# The unsafe tree is not yet fully tested, and is not used by default.
default = ["safe_tree"]
safe_tree = []
KGrewal1 marked this conversation as resolved.
Show resolved Hide resolved

[lints]
workspace = true
DJMcNab marked this conversation as resolved.
Show resolved Hide resolved
49 changes: 49 additions & 0 deletions tree_arena/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Tree Arena

This crate contains two implementations of a tree for use in masonry, one safe and the other unsafe. The safe tree is known to work, and serves as the baseline implementation and is used by default. The unsafe tree leverages a hashmap as an arena and is designed for higher performance: it leverages unsafe code to achieve this. The unsafe tree is not yet fully tested, and is not used by default.

The safe tree is the priority. This means:

* The safe version may have features / APIs that the unsafe version doesn't yet have.

* If both versions are at feature parity, Masonry can switch on the unsafe version for best performance.

* Otherwise, Masonry uses the safe version.

## Architecture

### Safe Tree

The safe tree contains a root `TreeArena` which owns the root nodes as `Vec<TreeNode<T>>`, and a`parents_map` tracking the parent of every node. Each `TreeNode` subsequently owns its own children as `Vec<TreeNode<T>>`. This model of owneship is thus checked by the rust compiler, but has the downside of requiring passing through every ancestor node to access the descendant - this requires an O(depth) determination of whether the node is a descendant, followed by O(children) time at each level to traverse the path to the child.

### Unsafe Tree

The unsafe tree arena contains a `DataMap` which **owns** all nodes. The `DataMap` contains:

* A `HashMap` associating `NodeId` with `Box<UnsafeCell<TreeNode<T>>>`, owning the node data, (boxed to prevent movement of the node when the `HashMap` is resized and `UnsafeCell` to express the interior mutability)

* A `HashMap` associating `NodeId` with `Option<NodeId>`, containing the parent information for the nodes

* `Box<UnsafeCell<Vec<NodeId>>>` containing the roots of the tree

It is possible to get shared (immutable) access or exclusive (mutable) access to the tree. These return `ArenaRef<'arena, T>` or `ArenaMut<'arena, T>` respectively. We do this by leveraging a hash map to store the nodes: from this we can obtain either shared or exclusive access to nodes. To ensure that only one item is allowed to create new exclusive access to nodes, this action requires mutable access to the arena as a whole (and so is checked by the compiler) - what the compiler cannot check is that the nodes accessed mutably are distinct from one another - this is done by only allowing access to descendants of the node being accessed mutably. The aim of this is to reduce the time needed to access node, as given a node, we only need to determine whether it is a descendant of the node being accessed mutably, and do not need to iterate over the children and to flatten the overall tree graph into a hash map.

#### Shared References

`ArenaRef<'arena, T>` contains the identity of the parent node, a reference to the node data, and `ArenaRefChildren<'arena, T>`. The `ArenaRefChildren<'arena, T>` contains the ids of the children of the node, the id of the node, and a reference to the arena. From this `ArenaRefChildren<'arena, T>` it is possible to get shared access to children of the node.

#### Exclusive References

`ArenaMut<'arena, T>` contains the identity of the parent node, a mutable reference to the node data, and `ArenaMutChildren<'arena, T>`. The `ArenaMutChildren<'arena, T>` contains the ids of the children of the node, the id of the node, and a mutable reference to the arena. From this `ArenaMutChildren<'arena, T>` it is possible to get exclusive access to children of the node.

#### Safety

From the `ArenaMutChildren<'arena, T>`, it is important that we can only access descendants of that node, such that we can only ever have exclusive mutable access to the contents of a node, and never have multiple mutable references. This invariant is not checked by the compiler and thus relies on the logic to determine whether a node is a descendant being correct.

### Complexity

|Operation | Safe | Unsafe |
| --- | --- | --- |
|Find child | O(Children) | O(1) |
|Descendant | O(Depth) | O(Depth) |
|From root | O(Depth) | O(1) |
23 changes: 23 additions & 0 deletions tree_arena/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0

//! This crate implements a tree data structure for use in Masonry
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to use cargo-rmde here; it's already set up in CI.

That doesn't need to block this PR, but it would be a good follow-up

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For clarity is that the content after the licence in the other lib.rs eg lines 4-22 in https://github.com/linebender/xilem/blob/main/xilem_core/src/lib.rs ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right. This is confusing.

No, this is the setup used specifically in the Masonry crate. The version in Xilem Core is an old version of that

//! It contains both a safe implementation (that is used by default)
//! and an unsafe implementation that can be used to improve performance
//!
//! The safe version is the first class citizen
//!
//! * The safe version may have features / APIs that the unsafe version doesn't yet have.
//! * If both versions are at feature parity, Masonry can switch on the unsafe version for best performance.
//! * Otherwise, Masonry uses the safe version.
type NodeId = u64;

#[cfg(not(feature = "safe_tree"))]
mod tree_arena_unsafe;
#[cfg(not(feature = "safe_tree"))]
pub use tree_arena_unsafe::*;

#[cfg(feature = "safe_tree")]
mod tree_arena_safe;
#[cfg(feature = "safe_tree")]
pub use tree_arena_safe::*;
Loading
Loading