summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHampusM <hampus@hampusmat.com>2026-10-09 12:24:23 +0200
committerHampusM <hampus@hampusmat.com>2026-10-09 15:10:54 +0200
commit59b4f377ec0175eb21757637347c2689b8a1012f (patch)
tree61567c1b6760da25c2672e7650ff7d87d3431986
parent923a113c66ac1b505990a9c35895b5f4ca903390 (diff)
refactor(engine): remove need to hash content of asset IDs
-rw-r--r--engine/src/asset.rs538
-rw-r--r--engine/src/model/asset.rs1
2 files changed, 300 insertions, 239 deletions
diff --git a/engine/src/asset.rs b/engine/src/asset.rs
index afa0ea4..8f7829c 100644
--- a/engine/src/asset.rs
+++ b/engine/src/asset.rs
@@ -1,10 +1,9 @@
use std::any::{type_name, Any, TypeId};
use std::borrow::Cow;
use std::cell::RefCell;
-use std::collections::HashMap;
use std::convert::Infallible;
use std::fmt::{Debug, Display};
-use std::hash::{DefaultHasher, Hash, Hasher};
+use std::hash::Hash;
use std::hint::cold_path;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
@@ -104,8 +103,8 @@ impl Display for Label<'_>
#[derive(Debug, Sole)]
pub struct Assets
{
- store: Vec<StoredAsset>,
- asset_lookup: RefCell<HashMap<LabelHash, LookupEntry>>,
+ store: store::Store,
+ metadata_lut: RefCell<hashbrown::HashMap<LabelOwned, Metadata>>,
importers: hashbrown::HashMap<AssetTypeId, WrappedImporterFn>,
import_work_queue: WorkQueue<ImportWorkUserData>,
import_work_msg_receiver: MpscReceiver<ImportWorkMessage>,
@@ -122,8 +121,8 @@ impl Assets
mpsc_channel::<ImportWorkMessage>();
Self {
- store: Vec::with_capacity(capacity),
- asset_lookup: RefCell::new(HashMap::with_capacity(capacity)),
+ store: store::Store::with_capacity(capacity),
+ metadata_lut: RefCell::new(hashbrown::HashMap::with_capacity(capacity)),
importers: hashbrown::HashMap::new(),
import_work_queue: WorkQueue::new("asset_importing_work_queue"),
import_work_msg_receiver,
@@ -154,22 +153,16 @@ impl Assets
{
let id = handle.id.into_valid()?;
- let asset_lookup = self.asset_lookup.borrow();
-
- let AssetState::Ready(asset_index, asset_label) =
- &asset_lookup.get(&id.label_hash)?.state
- else {
- return None;
- };
+ let stored_asset = self.store.get(id.index)?;
tracing::Span::current()
- .record("asset_label", tracing::field::display(&asset_label));
+ .record("asset_label", tracing::field::display(&stored_asset.label));
- let Some(stored_asset) = self.store.get(*asset_index) else {
- unreachable!();
+ let AssetState::Ready { data: asset_data } = &stored_asset.state else {
+ return None;
};
- let Some(asset) = stored_asset.inner.downcast_ref::<AssetT>() else {
+ let Some(asset) = asset_data.downcast_ref::<AssetT>() else {
tracing::error!("Wrong asset type");
return None;
};
@@ -187,22 +180,16 @@ impl Assets
{
let id = handle.id.into_valid()?;
- let asset_lookup = self.asset_lookup.borrow();
-
- let AssetState::Ready(asset_index, asset_label) =
- &asset_lookup.get(&id.label_hash)?.state
- else {
- return None;
- };
+ let stored_asset = self.store.get_mut(id.index)?;
tracing::Span::current()
- .record("asset_label", tracing::field::display(&asset_label));
+ .record("asset_label", tracing::field::display(&stored_asset.label));
- let Some(stored_asset) = self.store.get_mut(*asset_index) else {
- unreachable!();
+ let AssetState::Ready { data: asset_data } = &mut stored_asset.state else {
+ return None;
};
- let Some(asset) = stored_asset.inner.downcast_mut::<AssetT>() else {
+ let Some(asset) = asset_data.downcast_mut::<AssetT>() else {
tracing::error!("Wrong asset type");
return None;
};
@@ -222,25 +209,22 @@ impl Assets
tracing::Span::current().record("label", tracing::field::display(&label));
- let label_hash = LabelHash::new(&label);
-
- let asset_lookup = self.asset_lookup.borrow();
+ let (stored_asset, id) = self.get_asset_by_label(&label)?;
- let AssetState::Ready(asset_index, _) = &asset_lookup.get(&label_hash)?.state
+ let Some(StoredAsset {
+ state: AssetState::Ready { data: asset_data },
+ ..
+ }) = stored_asset
else {
return None;
};
- let Some(stored_asset) = self.store.get(*asset_index) else {
- unreachable!();
- };
-
- if stored_asset.inner.downcast_ref::<AssetT>().is_none() {
+ if asset_data.downcast_ref::<AssetT>().is_none() {
tracing::error!("Wrong asset type");
return None;
}
- Some(Handle::new(label_hash))
+ Some(Handle::from_id(Id::from_valid(id)))
}
pub fn is_loaded_and_has_type<AssetT>(&self, handle: &Handle<AssetT>) -> bool
@@ -251,19 +235,15 @@ impl Assets
return false;
};
- let asset_lookup = self.asset_lookup.borrow();
-
- let Some(AssetState::Ready(asset_index, _)) =
- asset_lookup.get(&id.label_hash).map(|entry| &entry.state)
+ let Some(StoredAsset {
+ state: AssetState::Ready { data: asset_data },
+ ..
+ }) = self.store.get(id.index)
else {
return false;
};
- let Some(stored_asset) = self.store.get(*asset_index) else {
- unreachable!();
- };
-
- stored_asset.inner.downcast_ref::<AssetT>().is_some()
+ asset_data.downcast_ref::<AssetT>().is_some()
}
pub fn get_label<AssetT>(&self, handle: &Handle<AssetT>) -> Option<Label<'static>>
@@ -277,13 +257,9 @@ impl Assets
{
let id = id.into_valid()?;
- let asset_lookup = self.asset_lookup.borrow();
+ let stored_asset = self.store.get(id.index)?;
- let AssetState::Ready(_, label) = &asset_lookup.get(&id.label_hash)?.state else {
- return None;
- };
-
- Some(label.clone().into_label())
+ Some(stored_asset.label.clone().into_label())
}
#[tracing::instrument(skip_all, fields(label, asset_type = %type_name::<AssetT>()))]
@@ -295,16 +271,9 @@ impl Assets
tracing::Span::current().record("label", tracing::field::display(&label));
- let label_hash = LabelHash::new(&label);
-
- self.load_asset::<Infallible>(
- AssetTypeId::of::<AssetT>(),
- label,
- label_hash,
- None,
- );
+ let id = self.load_asset::<Infallible>(AssetTypeId::of::<AssetT>(), label, None);
- Handle::new(label_hash)
+ Handle::from_id(Id::from_valid(id))
}
#[tracing::instrument(skip_all, fields(label, asset_type = %type_name::<AssetT>()))]
@@ -321,16 +290,10 @@ impl Assets
tracing::Span::current().record("label", tracing::field::display(&label));
- let label_hash = LabelHash::new(&label);
+ let id =
+ self.load_asset(AssetTypeId::of::<AssetT>(), label, Some(asset_settings));
- self.load_asset(
- AssetTypeId::of::<AssetT>(),
- label,
- label_hash,
- Some(asset_settings),
- );
-
- Handle::new(label_hash)
+ Handle::from_id(Id::from_valid(id))
}
pub fn store_with_name<'name, AssetT>(
@@ -382,58 +345,64 @@ impl Assets
tracing::Span::current().record("label", tracing::field::display(&label));
- let label_hash = LabelHash::new(&label);
+ let id = match self.get_asset_by_label(&label) {
+ Some((
+ Some(StoredAsset {
+ state:
+ AssetState::Ready { .. } | AssetState::DependenciesPending { .. },
+ ..
+ }),
+ id,
+ )) => {
+ tracing::error!("Asset already exists");
- let id = Id {
- inner: IdInner::Valid(IdValid { label_hash }),
+ return Handle::from_id(Id::from_valid(id));
+ }
+ Some((Some(StoredAsset { state: AssetState::Pending, .. }) | None, id)) => id,
+ None => IdValid::new(&self.store, &label),
};
- if matches!(
- self.asset_lookup
- .get_mut()
- .get(&label_hash)
- .map(|entry| &entry.state),
- Some(AssetState::Ready(..) | AssetState::DependenciesPending { .. })
- ) {
- tracing::error!("Asset already exists");
-
- return Handle::new(label_hash);
- }
-
let mut missing_dependencies = 0;
for dependency in asset.dependencies() {
- match self
- .asset_lookup
- .get_mut()
- .get_mut(&LabelHash::new(&dependency.label))
- {
- Some(LookupEntry { state: AssetState::Ready(..), .. }) => {}
- Some(LookupEntry {
- state: AssetState::Pending | AssetState::DependenciesPending { .. },
- waiting_dependants,
- }) => {
+ match self.get_asset_by_label_mut(&dependency.label) {
+ Some((Some(StoredAsset { state: AssetState::Ready { .. }, .. }), _)) => {}
+ Some((
+ Some(StoredAsset {
+ state:
+ AssetState::Pending | AssetState::DependenciesPending { .. },
+ waiting_dependants,
+ ..
+ }),
+ _,
+ )) => {
+ tracing::trace!(
+ dependency = %dependency.label,
+ "Asset has missing dependency"
+ );
+
missing_dependencies += 1;
waiting_dependants.push(id);
}
- None => {
- missing_dependencies += 1;
+ Some((None, _)) | None => {
+ tracing::trace!(
+ dependency = %dependency.label,
+ "Asset has missing dependency"
+ );
- let dependency_label_hash = LabelHash::new(&dependency.label);
+ missing_dependencies += 1;
- self.load_asset::<Infallible>(
+ let dependency_id = self.load_asset::<Infallible>(
dependency.asset_ty_id,
dependency.label.clone(),
- dependency_label_hash,
None,
);
- self.asset_lookup
- .get_mut()
- .entry(LabelHash::new(&dependency.label))
- .or_insert_with(|| LookupEntry {
+ self.store
+ .get_or_insert_with(dependency_id.index, || StoredAsset {
state: AssetState::Pending,
+ label: dependency.label.to_owned(),
waiting_dependants: Vec::new(),
})
.waiting_dependants
@@ -444,42 +413,55 @@ impl Assets
tracing::debug!("Storing asset");
- self.store.push(StoredAsset::new(asset));
-
- let index = self.store.len() - 1;
+ let stored_asset = self.store.get_or_insert_with(id.index, || StoredAsset {
+ state: AssetState::Pending,
+ label: label.to_owned(),
+ waiting_dependants: Vec::new(),
+ });
let label = label.to_owned();
- let entry = self
- .asset_lookup
- .get_mut()
- .entry(label_hash)
- .or_insert_with(|| LookupEntry {
- state: AssetState::Pending,
- waiting_dependants: Vec::new(),
- });
-
if missing_dependencies != 0 {
- entry.state = AssetState::DependenciesPending {
- index,
- label: label.clone(),
+ stored_asset.state = AssetState::DependenciesPending {
+ data: Box::new(asset),
remaining_dependencies: missing_dependencies,
};
} else {
- entry.state = AssetState::Ready(index, label.clone());
+ stored_asset.state = AssetState::Ready { data: Box::new(asset) };
- self.curr_tick_events
- .push(Event::Stored(id, label.into_label()));
+ self.curr_tick_events.push(Event::Stored(
+ Id::from_valid(id),
+ label.clone().into_label(),
+ ));
- let waiting_dependants = std::mem::take(&mut entry.waiting_dependants);
+ let waiting_dependants = std::mem::take(&mut stored_asset.waiting_dependants);
self.recurse_update_dependants(waiting_dependants);
}
- Handle::from_id(id)
+ let metadata_lut = self.metadata_lut.get_mut();
+
+ let parent_id = label.name.is_some().then(|| {
+ let parent_label = Label {
+ path: label.path.as_path().into(),
+ name: None,
+ };
+
+ metadata_lut
+ .entry(parent_label.to_owned())
+ .or_insert_with(|| Metadata {
+ id: IdValid::new(&self.store, &parent_label),
+ parent_id: None,
+ })
+ .id
+ });
+
+ metadata_lut.insert(label.clone(), Metadata { id, parent_id });
+
+ Handle::from_id(Id::from_valid(id))
}
- fn recurse_update_dependants(&mut self, initial_dependants: Vec<Id>)
+ fn recurse_update_dependants(&mut self, initial_dependants: Vec<IdValid>)
{
let mut all_dependants = initial_dependants;
@@ -491,18 +473,14 @@ impl Assets
}
for dependant_id in dependants {
- let dependant_label_hash = dependant_id.into_valid().unwrap().label_hash;
-
- let Some(LookupEntry { state, waiting_dependants }) =
- self.asset_lookup.get_mut().get_mut(&dependant_label_hash)
- else {
+ let Some(dependant_asset) = self.store.get_mut(dependant_id.index) else {
continue;
};
{
let AssetState::DependenciesPending {
remaining_dependencies, ..
- } = state
+ } = &mut dependant_asset.state
else {
continue;
};
@@ -514,79 +492,78 @@ impl Assets
}
}
- let AssetState::DependenciesPending { index, label, .. } =
- std::mem::replace(state, AssetState::Pending)
+ let AssetState::DependenciesPending { data: dependant_data, .. } =
+ std::mem::replace(&mut dependant_asset.state, AssetState::Pending)
else {
unreachable!();
};
- *state = AssetState::Ready(index, label.clone());
+ dependant_asset.state = AssetState::Ready { data: dependant_data };
- self.curr_tick_events
- .push(Event::Stored(dependant_id, label.into_label()));
+ self.curr_tick_events.push(Event::Stored(
+ Id::from_valid(dependant_id),
+ dependant_asset.label.clone().into_label(),
+ ));
- all_dependants.append(waiting_dependants);
+ all_dependants.append(&mut dependant_asset.waiting_dependants);
}
}
}
- fn is_pending(asset_lookup: &HashMap<LabelHash, LookupEntry>, label: &Label) -> bool
- {
- if label.name.is_some() {
- if let Some(AssetState::Pending | AssetState::DependenciesPending { .. }) =
- asset_lookup
- .get(&LabelHash::new(&Label {
- path: label.path.as_ref().into(),
- name: None,
- }))
- .map(|entry| &entry.state)
- {
- return true;
- }
- }
-
- if let Some(AssetState::Pending | AssetState::DependenciesPending { .. }) =
- asset_lookup
- .get(&LabelHash::new(label))
- .map(|entry| &entry.state)
- {
- return true;
- }
-
- false
- }
-
fn load_asset<AssetSettings>(
&self,
asset_ty_id: AssetTypeId,
label: Label<'_>,
- label_hash: LabelHash,
asset_settings: Option<AssetSettings>,
- ) where
+ ) -> IdValid
+ where
AssetSettings: Send + Sync + Debug + 'static,
{
- let mut asset_lookup = self.asset_lookup.borrow_mut();
+ let id = match self.get_asset_by_label(&label) {
+ Some((Some(_), id)) => {
+ return id;
+ }
+ Some((None, id)) => id,
+ None => IdValid::new(&self.store, &label),
+ };
- if Self::is_pending(&asset_lookup, &label) {
- return;
+ let mut metadata_lut = self.metadata_lut.borrow_mut();
+
+ let mut parent_exists = false;
+
+ let parent_id = label.name.is_some().then(|| {
+ let parent_label = Label {
+ path: label.path.as_ref().into(),
+ name: None,
+ };
+
+ parent_exists = metadata_lut.contains_key(&parent_label);
+
+ metadata_lut
+ .entry(parent_label.to_owned())
+ .or_insert_with(|| Metadata {
+ id: IdValid::new(&self.store, &parent_label),
+ parent_id: None,
+ })
+ .id
+ });
+
+ metadata_lut.insert(label.to_owned(), Metadata { id, parent_id });
+
+ if parent_exists {
+ return id;
}
- self.add_import_work::<AssetSettings>(
- asset_ty_id,
- &label,
- label_hash,
- asset_settings,
- &mut asset_lookup,
- );
+ self.add_import_work::<AssetSettings>(asset_ty_id, &label, asset_settings);
+
+ id
}
fn add_import_work<AssetSettings>(
&self,
asset_ty_id: AssetTypeId,
label: &Label<'_>,
- label_hash: LabelHash,
asset_settings: Option<AssetSettings>,
- asset_lookup: &mut HashMap<LabelHash, LookupEntry>,
) where
AssetSettings: Any + Send + Sync,
{
@@ -623,33 +600,30 @@ impl Assets
importer: importer.clone(),
},
});
+ }
+
+ fn get_asset_by_label(
+ &self,
+ label: &Label<'_>,
+ ) -> Option<(Option<&StoredAsset>, IdValid)>
+ {
+ let metadata_lut = self.metadata_lut.borrow();
- assert!(!asset_lookup.contains_key(&label_hash));
+ let id = metadata_lut.get(label)?.id;
- asset_lookup.insert(
- label_hash,
- LookupEntry {
- state: AssetState::Pending,
- waiting_dependants: Vec::new(),
- },
- );
+ Some((self.store.get(id.index), id))
+ }
- if label.name.is_some() {
- let parent_label_hash = LabelHash::new(&Label {
- path: label.path.as_ref().into(),
- name: None,
- });
+ fn get_asset_by_label_mut(
+ &mut self,
+ label: &Label<'_>,
+ ) -> Option<(Option<&mut StoredAsset>, IdValid)>
+ {
+ let metadata_lut = self.metadata_lut.get_mut();
- assert!(!asset_lookup.contains_key(&parent_label_hash));
+ let id = metadata_lut.get(label)?.id;
- asset_lookup.insert(
- parent_label_hash,
- LookupEntry {
- state: AssetState::Pending,
- waiting_dependants: Vec::new(),
- },
- );
- }
+ Some((self.store.get_mut(id.index), id))
}
}
@@ -724,6 +698,7 @@ pub struct Handle<AssetT: Asset>
impl<AssetT: Asset> Handle<AssetT>
{
+ #[must_use]
pub fn invalid() -> Self
{
Self::from_id(Id::invalid())
@@ -740,13 +715,6 @@ impl<AssetT: Asset> Handle<AssetT>
{
self.id
}
-
- fn new(label_hash: LabelHash) -> Self
- {
- Self::from_id(Id {
- inner: IdInner::Valid(IdValid { label_hash }),
- })
- }
}
impl<AssetT: Asset> Clone for Handle<AssetT>
@@ -766,6 +734,7 @@ pub struct Id
impl Id
{
+ #[must_use]
pub fn invalid() -> Self
{
Self { inner: IdInner::Invalid }
@@ -778,6 +747,11 @@ impl Id
IdInner::Invalid => None,
}
}
+
+ fn from_valid(id: IdValid) -> Self
+ {
+ Self { inner: IdInner::Valid(id) }
+ }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -790,7 +764,19 @@ enum IdInner
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct IdValid
{
- label_hash: LabelHash,
+ index: store::ReservedIndex,
+}
+
+impl IdValid
+{
+ fn new(store: &store::Store, label: &Label<'_>) -> Self
+ {
+ tracing::trace!("Generating valid ID for {label}");
+
+ let index = store.reserve_index();
+
+ Self { index }
+ }
}
#[derive(Debug, Default, Sole)]
@@ -890,21 +876,6 @@ impl WrappedImporterFn
}
}
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-struct LabelHash(u64);
-
-impl LabelHash
-{
- fn new(label: &Label<'_>) -> Self
- {
- let mut hasher = DefaultHasher::new();
-
- label.hash(&mut hasher);
-
- Self(hasher.finish())
- }
-}
-
#[derive(Debug, Default)]
pub(crate) struct Extension
{
@@ -982,21 +953,23 @@ enum ImportWorkMessage
}
#[derive(Debug)]
-struct LookupEntry
+struct Metadata
{
- state: AssetState,
- waiting_dependants: Vec<Id>,
+ id: IdValid,
+ parent_id: Option<IdValid>,
}
#[derive(Debug)]
enum AssetState
{
- Ready(usize, LabelOwned),
+ Ready
+ {
+ data: Box<dyn Any + Send + Sync>,
+ },
Pending,
DependenciesPending
{
- index: usize,
- label: LabelOwned,
+ data: Box<dyn Any + Send + Sync>,
remaining_dependencies: usize,
},
}
@@ -1004,15 +977,9 @@ enum AssetState
#[derive(Debug)]
struct StoredAsset
{
- inner: Box<dyn Any + Send + Sync>,
-}
-
-impl StoredAsset
-{
- fn new<AssetT: Asset>(asset: AssetT) -> Self
- {
- Self { inner: Box::new(asset) }
- }
+ state: AssetState,
+ label: LabelOwned,
+ waiting_dependants: Vec<IdValid>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -1029,7 +996,7 @@ impl AssetTypeId
}
}
-#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(Debug, Clone, Eq)]
struct LabelOwned
{
path: PathBuf,
@@ -1063,3 +1030,96 @@ impl Display for LabelOwned
<Label<'_> as Display>::fmt(&self.as_label(), formatter)
}
}
+
+impl PartialEq for LabelOwned
+{
+ fn eq(&self, other: &Self) -> bool
+ {
+ self.as_label() == other.as_label()
+ }
+}
+
+impl Hash for LabelOwned
+{
+ fn hash<HasherT: std::hash::Hasher>(&self, state: &mut HasherT)
+ {
+ <Label<'_> as Hash>::hash(&self.as_label(), state);
+ }
+}
+
+impl hashbrown::Equivalent<LabelOwned> for Label<'_>
+{
+ fn equivalent(&self, key: &LabelOwned) -> bool
+ {
+ self == &key.as_label()
+ }
+}
+
+mod store
+{
+ use std::num::NonZero;
+ use std::sync::atomic::{AtomicUsize, Ordering};
+
+ use super::StoredAsset;
+
+ #[derive(Debug)]
+ pub struct Store
+ {
+ store: Vec<Option<StoredAsset>>,
+ next_index: AtomicUsize,
+ }
+
+ impl Store
+ {
+ pub fn with_capacity(capacity: usize) -> Self
+ {
+ Self {
+ store: Vec::with_capacity(capacity),
+ next_index: AtomicUsize::new(1),
+ }
+ }
+
+ pub fn get(&self, index: ReservedIndex) -> Option<&StoredAsset>
+ {
+ self.store.get(index.0.get())?.as_ref()
+ }
+
+ pub fn get_mut(&mut self, index: ReservedIndex) -> Option<&mut StoredAsset>
+ {
+ self.store.get_mut(index.0.get())?.as_mut()
+ }
+
+ pub fn get_or_insert_with(
+ &mut self,
+ index: ReservedIndex,
+ func: impl FnOnce() -> StoredAsset,
+ ) -> &mut StoredAsset
+ {
+ let index = index.0.get();
+
+ let curr_len = self.store.len();
+
+ if curr_len <= index {
+ let missing = (index + 1) - curr_len;
+
+ self.store
+ .extend(std::iter::repeat_with(|| None).take(missing));
+ }
+
+ self.store[index].get_or_insert_with(func)
+ }
+
+ pub fn reserve_index(&self) -> ReservedIndex
+ {
+ let index = self.next_index.fetch_add(1, Ordering::Relaxed);
+
+ let index = NonZero::<usize>::new(index)
+ .expect("Asset index wrapped around to zero. Too many assets");
+
+ ReservedIndex(index)
+ }
+ }
+
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+ pub struct ReservedIndex(NonZero<usize>);
+}
diff --git a/engine/src/model/asset.rs b/engine/src/model/asset.rs
index d9de6ce..b000d7e 100644
--- a/engine/src/model/asset.rs
+++ b/engine/src/model/asset.rs
@@ -47,6 +47,7 @@ pub fn add_importers(assets: &mut Assets)
{
assets.set_importer::<Model, Settings, Error>(import);
assets.set_importer::<Mesh, Settings, Error>(import);
+ assets.set_importer::<MaterialDescription, Settings, Error>(import);
}
fn import(