Prefetch nearby scene assets in the background
This commit is contained in:
@@ -38,6 +38,7 @@ behavior:
|
||||
|
||||
vision:
|
||||
max_distance_meters: 64
|
||||
asset_cache_max_bytes: 2147483648
|
||||
reconnect:
|
||||
initial_delay_milliseconds: 1000
|
||||
maximum_delay_seconds: 60
|
||||
|
||||
@@ -37,3 +37,11 @@ Files: `src/grid_manager.rs`, `src/client_core.rs`.
|
||||
the existing UDP asset transfer path instead of reporting a missing asset immediately.
|
||||
|
||||
File: `src/asset_manager.rs`.
|
||||
|
||||
## Deterministic persistent-asset LRU
|
||||
|
||||
- Successful immutable asset-cache reads explicitly refresh the file modification timestamp.
|
||||
Pruning uses that timestamp as its cross-platform LRU order instead of filesystem access time,
|
||||
which can be disabled or coarsened by `noatime` and `relatime` mounts.
|
||||
|
||||
Files: `src/asset_cache.rs`, `src/asset_pipeline_semantics.rs`.
|
||||
|
||||
@@ -189,6 +189,10 @@ impl AssetCache {
|
||||
if bytes.len() as u64 != metadata.len() {
|
||||
return Err(Error::InvalidOperation);
|
||||
}
|
||||
// Use the modification timestamp as a portable, explicit LRU marker.
|
||||
// Filesystems commonly mount with relatime/noatime, so access time is
|
||||
// not reliable enough for cache eviction.
|
||||
let _ = fs::File::open(path).and_then(|file| file.set_modified(SystemTime::now()));
|
||||
Ok(Some(bytes))
|
||||
}
|
||||
|
||||
@@ -353,10 +357,7 @@ impl AssetCache {
|
||||
entries.push((
|
||||
entry.path(),
|
||||
metadata.len(),
|
||||
metadata
|
||||
.accessed()
|
||||
.or_else(|_| metadata.modified())
|
||||
.unwrap_or(UNIX_EPOCH),
|
||||
metadata.modified().unwrap_or(UNIX_EPOCH),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,13 +137,16 @@ fn custom_cache_naming_is_used_but_cannot_escape_the_cache_directory() {
|
||||
#[tokio::test]
|
||||
async fn cache_pruning_reclaims_old_entries_to_below_the_limit() {
|
||||
let (_client, cache, path) = fixture(12);
|
||||
let first = UUID::random().unwrap();
|
||||
let second = UUID::random().unwrap();
|
||||
cache
|
||||
.save_asset_to_cache_with_uuid_bytes(UUID::random().unwrap(), vec![1; 8])
|
||||
.save_asset_to_cache_with_uuid_bytes(first, vec![1; 8])
|
||||
.unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
cache
|
||||
.save_asset_to_cache_with_uuid_bytes(UUID::random().unwrap(), vec![2; 8])
|
||||
.save_asset_to_cache_with_uuid_bytes(second, vec![2; 8])
|
||||
.unwrap();
|
||||
cache.get_cached_asset_bytes_with_uuid(first).unwrap();
|
||||
cache.prune(None).await.unwrap();
|
||||
let bytes: u64 = fs::read_dir(&path)
|
||||
.unwrap()
|
||||
@@ -152,5 +155,7 @@ async fn cache_pruning_reclaims_old_entries_to_below_the_limit() {
|
||||
.map(|metadata| metadata.len())
|
||||
.sum();
|
||||
assert!(bytes <= 10);
|
||||
assert!(path.join(first.to_string()).is_file());
|
||||
assert!(!path.join(second.to_string()).exists());
|
||||
fs::remove_dir_all(path).unwrap();
|
||||
}
|
||||
|
||||
@@ -167,8 +167,26 @@ impl LibremetaverseClientOwner {
|
||||
///
|
||||
/// Returns a backend configuration error if the shared client defaults are invalid.
|
||||
pub fn new() -> Result<Self, BackendError> {
|
||||
Self::new_with_asset_cache_max_bytes(2 * 1024 * 1024 * 1024)
|
||||
}
|
||||
|
||||
/// Builds the shared client with the configured persistent asset-cache limit.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a backend configuration error for a zero or unsupported limit.
|
||||
pub fn new_with_asset_cache_max_bytes(max_bytes: u64) -> Result<Self, BackendError> {
|
||||
let mut settings = libremetaverse::Settings::default();
|
||||
settings.asset_cache_mut().dir = metacrate_cache_dir()?.to_string_lossy().into_owned();
|
||||
settings.asset_cache_mut().max_size =
|
||||
i64::try_from(max_bytes).map_err(|_| BackendError::Configuration {
|
||||
component: "persistent asset cache limit",
|
||||
})?;
|
||||
if settings.asset_cache().max_size == 0 {
|
||||
return Err(BackendError::Configuration {
|
||||
component: "persistent asset cache limit",
|
||||
});
|
||||
}
|
||||
let client = libremetaverse::GridClientBuilder::default()
|
||||
.with_settings(settings)
|
||||
.build()
|
||||
@@ -1552,12 +1570,15 @@ mod tests {
|
||||
#[cfg(feature = "live-grid")]
|
||||
#[test]
|
||||
fn asset_cache_is_platform_scoped_to_metacrate() {
|
||||
let cache = metacrate_cache_dir().expect("platform cache directory");
|
||||
let owner = LibremetaverseClientOwner::new().expect("client owner");
|
||||
let settings = owner.client().settings_ref().asset_cache();
|
||||
let cache = std::path::PathBuf::from(settings.dir);
|
||||
assert!(cache.is_absolute());
|
||||
assert!(
|
||||
cache
|
||||
.components()
|
||||
.any(|part| part.as_os_str() == "metacrate")
|
||||
);
|
||||
assert_eq!(settings.max_size, 2 * 1024 * 1024 * 1024);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +363,20 @@ impl AgentConfig {
|
||||
if !self.behavior.is_valid() {
|
||||
return Err(ConfigError::InvalidBehavior);
|
||||
}
|
||||
if !(64 * 1024 * 1024..=64 * 1024 * 1024 * 1024)
|
||||
.contains(&self.vision.asset_cache_max_bytes)
|
||||
{
|
||||
return Err(ConfigError::UnsafeLimit {
|
||||
field: "vision.asset_cache_max_bytes",
|
||||
value: self
|
||||
.vision
|
||||
.asset_cache_max_bytes
|
||||
.try_into()
|
||||
.unwrap_or(usize::MAX),
|
||||
minimum: 64 * 1024 * 1024,
|
||||
maximum: usize::try_from(64_u64 * 1024 * 1024 * 1024).unwrap_or(usize::MAX),
|
||||
});
|
||||
}
|
||||
if !self.vision.valid() {
|
||||
return Err(ConfigError::UnsafeLimit {
|
||||
field: "vision.max_distance_meters",
|
||||
@@ -1129,6 +1143,7 @@ struct RawBehavior {
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
struct RawVision {
|
||||
max_distance_meters: Option<u32>,
|
||||
asset_cache_max_bytes: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Deserialize)]
|
||||
@@ -1585,6 +1600,10 @@ fn resolve<E: Environment>(
|
||||
},
|
||||
vision: crate::vision::VisionLimits {
|
||||
max_distance_meters: raw.vision.max_distance_meters.unwrap_or(64),
|
||||
asset_cache_max_bytes: raw
|
||||
.vision
|
||||
.asset_cache_max_bytes
|
||||
.unwrap_or(2 * 1024 * 1024 * 1024),
|
||||
..crate::vision::VisionLimits::default()
|
||||
},
|
||||
reconnect,
|
||||
@@ -2011,6 +2030,17 @@ mod tests {
|
||||
..
|
||||
})
|
||||
));
|
||||
|
||||
let cache = temporary_file(
|
||||
"vision-cache.yml",
|
||||
"llm:\n endpoint_url: https://llm.invalid/chat\n api_key: placeholder\nvision:\n asset_cache_max_bytes: 1073741824\n",
|
||||
);
|
||||
let config = ConfigLoader::new()
|
||||
.with_file(&cache)
|
||||
.with_environment(MapEnvironment::default())
|
||||
.load()
|
||||
.expect("configured asset cache");
|
||||
assert_eq!(config.vision.asset_cache_max_bytes, 1024 * 1024 * 1024);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -281,7 +281,9 @@ async fn run_live(
|
||||
let connection = config.grid.clone().ok_or_else(|| {
|
||||
CliError("validated live configuration did not contain a grid connection".into())
|
||||
})?;
|
||||
let owner = LibremetaverseClientOwner::new()?;
|
||||
let owner = LibremetaverseClientOwner::new_with_asset_cache_max_bytes(
|
||||
config.vision.asset_cache_max_bytes,
|
||||
)?;
|
||||
let mut live = start_live_interactions(&config, &owner)?;
|
||||
let backend = match owner.session_backend_with_agent_services(
|
||||
connection,
|
||||
@@ -291,6 +293,7 @@ async fn run_live(
|
||||
) {
|
||||
Ok(backend) => backend,
|
||||
Err(error) => {
|
||||
live.scene_source.stop_prefetch().await;
|
||||
live.interaction.shutdown().await?;
|
||||
live.behavior.shutdown().await?;
|
||||
return Err(error.into());
|
||||
@@ -348,6 +351,7 @@ async fn run_live(
|
||||
Err(_) => Err(CliError("timed out waiting for full grid readiness".into())),
|
||||
};
|
||||
if let Err(error) = readiness {
|
||||
live.scene_source.stop_prefetch().await;
|
||||
let session_result = handle.shutdown().await;
|
||||
let interaction_result = live.interaction.shutdown().await;
|
||||
let behavior_result = live.behavior.shutdown().await;
|
||||
@@ -356,6 +360,7 @@ async fn run_live(
|
||||
behavior_result?;
|
||||
return Err(error.into());
|
||||
}
|
||||
live.scene_source.stop_prefetch().await;
|
||||
let session_result = handle.shutdown().await;
|
||||
let interaction_result = live.interaction.shutdown().await;
|
||||
let behavior_result = live.behavior.shutdown().await;
|
||||
@@ -562,6 +567,7 @@ async fn run_live(
|
||||
let _ = live.observability.record(shutdown_event);
|
||||
control_target.mark_stopping();
|
||||
live.landmark_roaming.shutdown().await;
|
||||
live.scene_source.stop_prefetch().await;
|
||||
let session_result = handle.shutdown().await;
|
||||
let interaction_result = live.interaction.shutdown().await;
|
||||
let behavior_result = live.behavior.shutdown().await;
|
||||
@@ -600,6 +606,7 @@ struct LiveInteractions {
|
||||
build_control: Arc<dyn metacrate_grid_agent::BuildControl>,
|
||||
vision:
|
||||
Arc<metacrate_grid_agent::VisionService<metacrate_grid_agent::LibremetaverseSceneSource>>,
|
||||
scene_source: Arc<metacrate_grid_agent::LibremetaverseSceneSource>,
|
||||
world: Arc<metacrate_grid_agent::LibremetaverseWorldSnapshotSource>,
|
||||
}
|
||||
|
||||
@@ -740,13 +747,10 @@ fn start_live_interactions(
|
||||
config.storage_path.join("mentra"),
|
||||
)?);
|
||||
let vision_limits = config.vision;
|
||||
let vision = Arc::new(
|
||||
VisionService::new(
|
||||
Arc::new(LibremetaverseSceneSource::new(owner, vision_limits)),
|
||||
vision_limits,
|
||||
)?
|
||||
.prefer_gpu(),
|
||||
);
|
||||
let scene_source = Arc::new(LibremetaverseSceneSource::new(owner, vision_limits));
|
||||
scene_source.start_prefetch();
|
||||
let vision =
|
||||
Arc::new(VisionService::new(Arc::clone(&scene_source), vision_limits)?.prefer_gpu());
|
||||
let responder = Arc::new(VisionAugmentedResponder::new(vision.clone(), responder));
|
||||
let sink = Arc::new(owner.interaction_sink());
|
||||
let pacer: Arc<dyn metacrate_grid_agent::ResponsePacer> = Arc::new(behavior_ingress);
|
||||
@@ -776,6 +780,7 @@ fn start_live_interactions(
|
||||
landmark_roaming,
|
||||
build_control,
|
||||
vision,
|
||||
scene_source,
|
||||
world,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ pub struct VisionLimits {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub max_distance_meters: u32,
|
||||
pub asset_cache_max_bytes: u64,
|
||||
pub max_entities: usize,
|
||||
pub max_triangles: usize,
|
||||
pub max_texture_fetches: usize,
|
||||
@@ -98,6 +99,7 @@ impl Default for VisionLimits {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
max_distance_meters: 64,
|
||||
asset_cache_max_bytes: 2 * 1024 * 1024 * 1024,
|
||||
max_entities: 2_048,
|
||||
max_triangles: 1_048_576,
|
||||
max_texture_fetches: 1_024,
|
||||
@@ -116,6 +118,7 @@ impl VisionLimits {
|
||||
&& (64..=1_080).contains(&self.height)
|
||||
&& self.width as usize * self.height as usize <= 2_073_600
|
||||
&& (1..=1_024).contains(&self.max_distance_meters)
|
||||
&& (64 * 1024 * 1024..=64 * 1024 * 1024 * 1024).contains(&self.asset_cache_max_bytes)
|
||||
&& (1..=4_096).contains(&self.max_entities)
|
||||
&& (1..=1_048_576).contains(&self.max_triangles)
|
||||
&& self.max_texture_fetches <= 1_024
|
||||
@@ -945,6 +948,8 @@ pub struct LibremetaverseSceneSource {
|
||||
agent: Arc<libremetaverse::AgentManager>,
|
||||
limits: VisionLimits,
|
||||
cache: Arc<Mutex<SceneCache>>,
|
||||
prefetch_task: Mutex<Option<tokio::task::JoinHandle<()>>>,
|
||||
prefetch_passes: AtomicU64,
|
||||
}
|
||||
|
||||
#[cfg(feature = "live-grid")]
|
||||
@@ -954,6 +959,8 @@ struct SceneCache {
|
||||
textures: std::collections::BTreeMap<UUID, CachedTexture>,
|
||||
texture_pixels: usize,
|
||||
geometry: std::collections::HashMap<UUID, CachedGeometry>,
|
||||
legacy_materials: std::collections::BTreeMap<UUID, libremetaverse::materials::LegacyMaterial>,
|
||||
pbr_materials: std::collections::BTreeMap<UUID, libremetaverse::assets::AssetMaterial>,
|
||||
access: u64,
|
||||
}
|
||||
|
||||
@@ -989,6 +996,57 @@ impl LibremetaverseSceneSource {
|
||||
agent: owner.agent(),
|
||||
limits,
|
||||
cache: Arc::new(Mutex::new(SceneCache::default())),
|
||||
prefetch_task: Mutex::new(None),
|
||||
prefetch_passes: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Continuously warms the same asset, decode, material, and geometry caches used by capture.
|
||||
pub fn start_prefetch(self: &Arc<Self>) {
|
||||
let mut task = lock(&self.prefetch_task);
|
||||
if task.is_some() {
|
||||
return;
|
||||
}
|
||||
let source = Arc::downgrade(self);
|
||||
*task = Some(tokio::spawn(async move {
|
||||
loop {
|
||||
let Some(source) = source.upgrade() else {
|
||||
break;
|
||||
};
|
||||
if source.client.network().current_sim().is_some()
|
||||
&& source
|
||||
.capture_scene(0, CancellationToken::default())
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
source.prefetch_passes.fetch_add(1, Ordering::AcqRel);
|
||||
}
|
||||
drop(source);
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/// Stops background prefetching before logout or runtime shutdown.
|
||||
pub async fn stop_prefetch(&self) {
|
||||
let task = lock(&self.prefetch_task).take();
|
||||
if let Some(task) = task {
|
||||
task.abort();
|
||||
let _ = task.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn prefetch_passes(&self) -> u64 {
|
||||
self.prefetch_passes.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "live-grid")]
|
||||
impl Drop for LibremetaverseSceneSource {
|
||||
fn drop(&mut self) {
|
||||
if let Some(task) = lock(&self.prefetch_task).take() {
|
||||
task.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1065,7 +1123,11 @@ impl SceneSource for LibremetaverseSceneSource {
|
||||
}
|
||||
}
|
||||
let camera = &self.agent.movement.camera;
|
||||
let position = camera.position();
|
||||
let position = if generation == 0 {
|
||||
self.agent.sim_position()
|
||||
} else {
|
||||
camera.position()
|
||||
};
|
||||
let forward = native_camera_forward(camera);
|
||||
let up = camera.up_axis();
|
||||
let entity_limit = self.limits.max_entities.saturating_sub(2);
|
||||
@@ -1186,6 +1248,8 @@ impl SceneSource for LibremetaverseSceneSource {
|
||||
cache.textures.clear();
|
||||
cache.texture_pixels = 0;
|
||||
cache.geometry.clear();
|
||||
cache.legacy_materials.clear();
|
||||
cache.pbr_materials.clear();
|
||||
cache.access = 0;
|
||||
}
|
||||
}
|
||||
@@ -1276,19 +1340,36 @@ impl SceneSource for LibremetaverseSceneSource {
|
||||
ids
|
||||
});
|
||||
let assets = self.client.assets();
|
||||
pbr_material_ids.truncate(self.limits.max_texture_fetches / 4);
|
||||
let legacy_material_fetches = usize::from(!legacy_material_ids.is_empty());
|
||||
let legacy_materials = if legacy_material_ids.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
self.client
|
||||
let missing_legacy_materials = {
|
||||
let cache = lock(&self.cache);
|
||||
legacy_material_ids
|
||||
.iter()
|
||||
.any(|id| !cache.legacy_materials.contains_key(id))
|
||||
};
|
||||
let legacy_material_fetches = usize::from(missing_legacy_materials);
|
||||
if missing_legacy_materials {
|
||||
let fetched = self
|
||||
.client
|
||||
.objects()
|
||||
.request_materials_with_simulator_cancellation_token(
|
||||
simulator.clone(),
|
||||
Some(cancellation.clone()),
|
||||
)
|
||||
.await
|
||||
.map_or_else(|_| Vec::new(), Iterator::collect)
|
||||
.map_or_else(|_| Vec::new(), Iterator::collect::<Vec<_>>);
|
||||
let mut cache = lock(&self.cache);
|
||||
cache.legacy_materials.extend(
|
||||
fetched
|
||||
.into_iter()
|
||||
.map(|material| (material.id(), material)),
|
||||
);
|
||||
}
|
||||
let legacy_materials = {
|
||||
let cache = lock(&self.cache);
|
||||
legacy_material_ids
|
||||
.iter()
|
||||
.filter_map(|id| cache.legacy_materials.get(id).cloned())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
for material in &legacy_materials {
|
||||
for id in [material.normal_map(), material.specular_map()] {
|
||||
@@ -1298,7 +1379,31 @@ impl SceneSource for LibremetaverseSceneSource {
|
||||
}
|
||||
}
|
||||
let mut texture_bytes = 0usize;
|
||||
let mut pbr_materials = Vec::new();
|
||||
let mut pbr_materials = {
|
||||
let cache = lock(&self.cache);
|
||||
pbr_material_ids
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
cache
|
||||
.pbr_materials
|
||||
.get(id)
|
||||
.cloned()
|
||||
.map(|value| (*id, value))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
for (_, material) in &pbr_materials {
|
||||
for texture in material.texture_ids() {
|
||||
if texture != UUID::zero() && !texture_ids.contains(&texture) {
|
||||
texture_ids.push(texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let cache = lock(&self.cache);
|
||||
pbr_material_ids.retain(|id| !cache.pbr_materials.contains_key(id));
|
||||
}
|
||||
pbr_material_ids.truncate(self.limits.max_texture_fetches / 4);
|
||||
let mut pending_materials = tokio::task::JoinSet::new();
|
||||
let mut material_jobs = std::collections::VecDeque::from(pbr_material_ids.clone());
|
||||
for _ in 0..16 {
|
||||
@@ -1337,6 +1442,7 @@ impl SceneSource for LibremetaverseSceneSource {
|
||||
texture_ids.push(texture);
|
||||
}
|
||||
}
|
||||
lock(&self.cache).pbr_materials.insert(id, material.clone());
|
||||
pbr_materials.push((id, material));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,22 @@ fn uuid(value: u32) -> UUID {
|
||||
UUID::new_with_string(format!("00000000-0000-4000-8000-{value:012}")).unwrap()
|
||||
}
|
||||
|
||||
#[cfg(feature = "live-grid")]
|
||||
#[tokio::test]
|
||||
async fn background_prefetch_is_idempotent_and_stoppable_without_a_simulator() {
|
||||
let owner = crate::backend::LibremetaverseClientOwner::new().unwrap();
|
||||
let source = Arc::new(LibremetaverseSceneSource::new(
|
||||
&owner,
|
||||
VisionLimits::default(),
|
||||
));
|
||||
source.start_prefetch();
|
||||
source.start_prefetch();
|
||||
tokio::task::yield_now().await;
|
||||
source.stop_prefetch().await;
|
||||
source.stop_prefetch().await;
|
||||
assert_eq!(source.prefetch_passes(), 0);
|
||||
}
|
||||
|
||||
fn triangle(z: f32, color: [u8; 4]) -> SceneTriangle {
|
||||
SceneTriangle {
|
||||
vertices: [[-1.0, -1.0, z], [1.0, -1.0, z], [0.0, 1.0, z]],
|
||||
|
||||
@@ -111,6 +111,13 @@ async fn live_bevy_capture_renders_in_front_of_avatar() -> Result<(), Box<dyn Er
|
||||
let renderer_owner = LibremetaverseClientOwner::new()?;
|
||||
let (mut renderer_session, renderer_cancel) =
|
||||
login(&renderer_owner, &environment, "GRID_USER", "GRID_PASSWORD").await?;
|
||||
let limits = VisionLimits {
|
||||
minimum_interval: Duration::ZERO,
|
||||
..VisionLimits::default()
|
||||
};
|
||||
let scene_source = Arc::new(LibremetaverseSceneSource::new(&renderer_owner, limits));
|
||||
let prefetch_started = Instant::now();
|
||||
scene_source.start_prefetch();
|
||||
|
||||
let result = async {
|
||||
wait_ready("renderer", &mut renderer_session, &renderer_cancel).await?;
|
||||
@@ -143,17 +150,26 @@ async fn live_bevy_capture_renders_in_front_of_avatar() -> Result<(), Box<dyn Er
|
||||
.camera
|
||||
.set_vertical_fov_angle(65_f32.to_radians())?;
|
||||
|
||||
let limits = VisionLimits {
|
||||
minimum_interval: Duration::ZERO,
|
||||
..VisionLimits::default()
|
||||
};
|
||||
tokio::time::timeout(Duration::from_mins(10), async {
|
||||
while scene_source.prefetch_passes() == 0 {
|
||||
tokio::time::sleep(Duration::from_millis(250)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| "background scene prefetch did not complete")?;
|
||||
println!(
|
||||
"LIVE_RENDER_PREFETCH_MILLISECONDS={}",
|
||||
prefetch_started.elapsed().as_millis()
|
||||
);
|
||||
scene_source.stop_prefetch().await;
|
||||
|
||||
println!(
|
||||
"LIVE_RENDER_SCOPE=max_distance_meters:{}",
|
||||
limits.max_distance_meters
|
||||
);
|
||||
let vision = VisionService::new(
|
||||
Arc::new(DiagnosticSceneSource {
|
||||
inner: LibremetaverseSceneSource::new(&renderer_owner, limits),
|
||||
inner: Arc::clone(&scene_source),
|
||||
material_override: std::env::var("METACRATE_LIVE_RENDER_MATERIAL_OVERRIDE").ok(),
|
||||
}),
|
||||
limits,
|
||||
@@ -173,6 +189,7 @@ async fn live_bevy_capture_renders_in_front_of_avatar() -> Result<(), Box<dyn Er
|
||||
let mut previous_missing = u32::MAX;
|
||||
let mut stable_missing = 0_u32;
|
||||
for frame in 1..=frames {
|
||||
let frame_started = Instant::now();
|
||||
let next = vision
|
||||
.capture(
|
||||
&format!("live-renderer-evidence-{frame}"),
|
||||
@@ -180,6 +197,10 @@ async fn live_bevy_capture_renders_in_front_of_avatar() -> Result<(), Box<dyn Er
|
||||
renderer_cancel.token(),
|
||||
)
|
||||
.await?;
|
||||
println!(
|
||||
"LIVE_RENDER_FRAME_{frame}_MILLISECONDS={}",
|
||||
frame_started.elapsed().as_millis()
|
||||
);
|
||||
println!("LIVE_RENDER_FRAME_{frame}_SUMMARY={}", next.summary);
|
||||
std::fs::write(&output, &next.jpeg)?;
|
||||
if next.completeness.textures_missing == previous_missing {
|
||||
@@ -200,12 +221,13 @@ async fn live_bevy_capture_renders_in_front_of_avatar() -> Result<(), Box<dyn Er
|
||||
}
|
||||
.await;
|
||||
|
||||
scene_source.stop_prefetch().await;
|
||||
let _ = renderer_session.logout(renderer_cancel.token()).await;
|
||||
result
|
||||
}
|
||||
|
||||
struct DiagnosticSceneSource {
|
||||
inner: LibremetaverseSceneSource,
|
||||
inner: Arc<LibremetaverseSceneSource>,
|
||||
material_override: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,14 @@ objects are selected by that distance; avatar attachments are rigged through
|
||||
their linkset hierarchy and included only when the camera is focused on the
|
||||
avatar, after world-object capacity is reserved.
|
||||
|
||||
Raw mesh, texture, and material assets are treated as immutable by UUID and persist in the
|
||||
platform MetaCrate cache. `vision.asset_cache_max_bytes` bounds that cache with a 2 GiB default;
|
||||
successful reads refresh its LRU order and pruning removes the least recently used assets first.
|
||||
While logged in, the live scene source continuously runs its normal distance-bounded collection,
|
||||
asset fetch, texture decode, material parse, and mesh reconstruction path in the background. A
|
||||
snapshot therefore reuses the warm persistent, decoded, geometry, material, and GPU caches instead
|
||||
of beginning asset discovery when the model asks to look.
|
||||
|
||||
`behavior_camera_set` gives an authorized model a bounded position, target, and
|
||||
vertical field of view; `behavior_camera_reset` restores the avatar-facing
|
||||
view. Full avatar body/attachment mesh fidelity and baking remain separately
|
||||
|
||||
@@ -55,7 +55,11 @@ deployments should use absolute paths.
|
||||
|
||||
Downloaded grid assets use the platform cache directory: `$XDG_CACHE_HOME/metacrate`
|
||||
or `$HOME/.cache/metacrate` on Linux, `$HOME/Library/Caches/metacrate` on macOS,
|
||||
and `%LOCALAPPDATA%\metacrate\cache` on Windows.
|
||||
and `%LOCALAPPDATA%\metacrate\cache` on Windows. The cache treats UUID-addressed assets as
|
||||
immutable, defaults to a 2 GiB LRU limit, and is configurable with
|
||||
`vision.asset_cache_max_bytes`. The live scene source continuously prefetches and prepares assets
|
||||
inside the configured view radius while the avatar is logged in; prefetching is stopped before
|
||||
logout and shutdown.
|
||||
|
||||
## Endpoint, grid identity, and authority
|
||||
|
||||
|
||||
Reference in New Issue
Block a user