Skip to content

Commit f9282d8

Browse files
authored
Improve resource loading persistency and feedback (#7)
* ported to the new bevy 0.19 * Improved the resource loading
1 parent 8163745 commit f9282d8

2 files changed

Lines changed: 169 additions & 70 deletions

File tree

examples/cu_flight_controller/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
/assets/city-fixed.glb
44
/assets/skybox.ktx2
55
/assets/specular_map.ktx2
6+
/assets/.download-cache/

examples/cu_flight_controller/src/sim.rs

Lines changed: 168 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ use cu_sensor_payloads::{BarometerPayload, ImuPayload, MagnetometerPayload};
5858
#[cfg(not(target_arch = "wasm32"))]
5959
use std::fs;
6060
#[cfg(not(target_arch = "wasm32"))]
61-
use std::io;
61+
use std::io::{self, Write};
6262
#[cfg(not(target_arch = "wasm32"))]
6363
use std::path::{Path, PathBuf};
6464
use std::sync::atomic::Ordering;
@@ -96,6 +96,14 @@ struct CopperState {
9696
app: gnss::FlightControllerSim,
9797
}
9898

99+
#[derive(Clone, Resource)]
100+
struct SceneAssetPaths {
101+
quadcopter: String,
102+
city: String,
103+
skybox: String,
104+
specular_map: String,
105+
}
106+
99107
#[derive(Resource, Default, Clone)]
100108
struct SimMotorCommands {
101109
dshot: [u16; 4],
@@ -397,6 +405,9 @@ const SKYBOX: &str = "skybox.ktx2";
397405
const SPECULAR_MAP: &str = "specular_map.ktx2";
398406
const QUADCOPTER: &str = "quadcopter.glb";
399407
const CITY: &str = "city-fixed.glb";
408+
const SCENE_ASSETS: [&str; 4] = [QUADCOPTER, CITY, SKYBOX, SPECULAR_MAP];
409+
#[cfg(not(target_arch = "wasm32"))]
410+
const SCENE_ASSET_CACHE_DIR: &str = ".download-cache";
400411
// Measured from `gltf-transform inspect city.glb` in source model units.
401412
const LOCAL_CITY_BBOX_MIN_UNITS: Vec3 = Vec3::new(-30_614.165, -648.2196, -4_185.883);
402413
const LOCAL_CITY_BBOX_MAX_UNITS: Vec3 = Vec3::new(18_754.953, 11_102.407, 35_871.875);
@@ -442,21 +453,62 @@ fn spawn_pose_components() -> (
442453
}
443454

444455
#[cfg(not(target_arch = "wasm32"))]
445-
fn create_symlink(src: &str, dst: &str) -> io::Result<()> {
446-
let dst_path = Path::new(dst);
456+
fn scene_asset_root() -> PathBuf {
457+
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets")
458+
}
459+
460+
#[cfg(not(target_arch = "wasm32"))]
461+
fn scene_asset_cache_root() -> PathBuf {
462+
scene_asset_root().join(SCENE_ASSET_CACHE_DIR)
463+
}
464+
465+
#[cfg(not(target_arch = "wasm32"))]
466+
fn path_string(path: &Path) -> String {
467+
path.to_string_lossy().into_owned()
468+
}
469+
470+
#[cfg(not(target_arch = "wasm32"))]
471+
fn asset_progress_bar(done: usize, total: usize) -> String {
472+
const WIDTH: usize = 28;
473+
let filled = WIDTH * done / total.max(1);
474+
let empty = WIDTH.saturating_sub(filled);
475+
format!(
476+
"[{}{}] {done}/{total}",
477+
"=".repeat(filled),
478+
" ".repeat(empty)
479+
)
480+
}
447481

448-
if dst_path.exists() {
449-
fs::remove_file(dst_path)?;
482+
#[cfg(not(target_arch = "wasm32"))]
483+
fn link_or_copy_cached_asset(src: &Path, dst: &Path) -> io::Result<()> {
484+
if fs::symlink_metadata(dst).is_ok() {
485+
fs::remove_file(dst)?;
450486
}
451487

452488
#[cfg(unix)]
453489
{
454-
std::os::unix::fs::symlink(src, dst)
490+
match std::os::unix::fs::symlink(src, dst) {
491+
Ok(()) => Ok(()),
492+
Err(symlink_err) => fs::copy(src, dst).map(|_| ()).map_err(|copy_err| {
493+
io::Error::new(
494+
copy_err.kind(),
495+
format!("failed to symlink ({symlink_err}) or copy ({copy_err})"),
496+
)
497+
}),
498+
}
455499
}
456500

457501
#[cfg(windows)]
458502
{
459-
std::os::windows::fs::symlink_file(src, dst)
503+
match std::os::windows::fs::symlink_file(src, dst) {
504+
Ok(()) => Ok(()),
505+
Err(symlink_err) => fs::copy(src, dst).map(|_| ()).map_err(|copy_err| {
506+
io::Error::new(
507+
copy_err.kind(),
508+
format!("failed to symlink ({symlink_err}) or copy ({copy_err})"),
509+
)
510+
}),
511+
}
460512
}
461513
}
462514

@@ -467,18 +519,15 @@ fn get_asset_path(
467519
asset_url: &str,
468520
asset_name: &str,
469521
) -> Result<PathBuf, CacheError> {
470-
match online_cache.cached_path(asset_url) {
522+
match offline_cache.cached_path(asset_url) {
471523
Ok(path) => Ok(path),
472524
Err(err) => {
473525
if matches!(
474526
err,
475-
CacheError::HttpError(_) | CacheError::IoError(_) | CacheError::ResourceNotFound(_)
527+
CacheError::NoCachedVersions(_) | CacheError::CacheCorrupted(_)
476528
) {
477-
eprintln!(
478-
"Failed to fetch latest '{}' from network ({}). Attempting to use cached version.",
479-
asset_name, err
480-
);
481-
offline_cache.cached_path(asset_url)
529+
eprintln!(" {asset_name}: cache miss; downloading from {asset_url}");
530+
online_cache.cached_path(asset_url)
482531
} else {
483532
Err(err)
484533
}
@@ -490,14 +539,92 @@ fn get_asset_path(
490539
fn precached_asset_path(
491540
online_cache: &Cache,
492541
offline_cache: &Cache,
542+
asset_root: &Path,
543+
index: usize,
544+
total: usize,
493545
asset_name: &str,
494-
) -> Result<PathBuf, CacheError> {
546+
) -> Result<String, CacheError> {
547+
let plain_path = asset_root.join(asset_name);
548+
if plain_path.is_file() {
549+
eprintln!(
550+
" {} {asset_name}: cached",
551+
asset_progress_bar(index, total)
552+
);
553+
return Ok(path_string(&plain_path));
554+
}
555+
556+
if fs::symlink_metadata(&plain_path).is_ok() {
557+
fs::remove_file(&plain_path)?;
558+
}
559+
560+
eprintln!(
561+
" {} {asset_name}: resolving",
562+
asset_progress_bar(index.saturating_sub(1), total)
563+
);
495564
let asset_url = format!("{BASE_ASSETS_URL}{asset_name}");
496565
let hashed_path = get_asset_path(online_cache, offline_cache, &asset_url, asset_name)?;
497-
let plain_path = hashed_path.parent().unwrap().join(asset_name);
498-
create_symlink(hashed_path.to_str().unwrap(), plain_path.to_str().unwrap())
499-
.expect("failed to create cached-asset symlink");
500-
Ok(plain_path)
566+
link_or_copy_cached_asset(&hashed_path, &plain_path)?;
567+
eprintln!(" {} {asset_name}: ready", asset_progress_bar(index, total));
568+
Ok(path_string(&plain_path))
569+
}
570+
571+
#[cfg(not(target_arch = "wasm32"))]
572+
fn prepare_scene_assets() -> SceneAssetPaths {
573+
let asset_root = scene_asset_root();
574+
let cache_root = scene_asset_cache_root();
575+
fs::create_dir_all(&asset_root).expect("failed to create scene asset directory");
576+
577+
eprintln!(
578+
"Preparing Copper flight-controller scene assets in {}",
579+
asset_root.display()
580+
);
581+
let _ = io::stderr().flush();
582+
583+
let online_cache = Cache::builder()
584+
.dir(cache_root.clone())
585+
.progress_bar(Some(ProgressBar::Full))
586+
.build()
587+
.expect("failed to create online scene asset cache");
588+
let offline_cache = Cache::builder()
589+
.dir(cache_root)
590+
.offline(true)
591+
.progress_bar(None)
592+
.build()
593+
.expect("failed to create offline scene asset cache");
594+
595+
let total = SCENE_ASSETS.len();
596+
let mut paths = Vec::with_capacity(total);
597+
for (i, asset_name) in SCENE_ASSETS.iter().enumerate() {
598+
paths.push(
599+
precached_asset_path(
600+
&online_cache,
601+
&offline_cache,
602+
&asset_root,
603+
i + 1,
604+
total,
605+
asset_name,
606+
)
607+
.unwrap_or_else(|err| panic!("failed to prepare {asset_name}: {err}")),
608+
);
609+
}
610+
611+
eprintln!("Scene assets ready.");
612+
SceneAssetPaths {
613+
quadcopter: paths[0].clone(),
614+
city: paths[1].clone(),
615+
skybox: paths[2].clone(),
616+
specular_map: paths[3].clone(),
617+
}
618+
}
619+
620+
#[cfg(target_arch = "wasm32")]
621+
fn prepare_scene_assets() -> SceneAssetPaths {
622+
SceneAssetPaths {
623+
quadcopter: QUADCOPTER.to_string(),
624+
city: CITY.to_string(),
625+
skybox: SKYBOX.to_string(),
626+
specular_map: SPECULAR_MAP.to_string(),
627+
}
501628
}
502629

503630
fn propeller_from_position(x: f32, y: f32, z: f32, direction: RotationDirection) -> PropellerInfo {
@@ -696,52 +823,15 @@ fn setup_world(
696823
asset_server: Res<AssetServer>,
697824
mut images: ResMut<Assets<Image>>,
698825
layout: Res<WorldLayout>,
826+
asset_paths: Res<SceneAssetPaths>,
699827
) {
700-
#[cfg(not(target_arch = "wasm32"))]
701-
let online_cache = Cache::builder()
702-
.progress_bar(Some(ProgressBar::Full))
703-
.build()
704-
.expect("failed to create online file cache");
705-
706-
#[cfg(not(target_arch = "wasm32"))]
707-
let offline_cache = Cache::builder()
708-
.progress_bar(Some(ProgressBar::Full))
709-
.offline(true)
710-
.build()
711-
.expect("failed to create offline file cache");
712-
713-
#[cfg(not(target_arch = "wasm32"))]
714-
let quadcopter_path = precached_asset_path(&online_cache, &offline_cache, QUADCOPTER)
715-
.expect("failed to get quadcopter.glb (online or cached)");
716-
#[cfg(not(target_arch = "wasm32"))]
717-
let skybox_path = precached_asset_path(&online_cache, &offline_cache, SKYBOX)
718-
.expect("failed to get skybox.ktx2 (online or cached)");
719-
#[cfg(not(target_arch = "wasm32"))]
720-
let specular_map_path = precached_asset_path(&online_cache, &offline_cache, SPECULAR_MAP)
721-
.expect("failed to get specular_map.ktx2 (online or cached)");
722-
#[cfg(not(target_arch = "wasm32"))]
723-
let city_path = precached_asset_path(&online_cache, &offline_cache, CITY)
724-
.expect("failed to get city.glb (online or cached)");
725-
#[cfg(target_arch = "wasm32")]
726-
let quadcopter_path = QUADCOPTER;
727-
#[cfg(target_arch = "wasm32")]
728-
let skybox_path = SKYBOX;
729-
#[cfg(target_arch = "wasm32")]
730-
let specular_map_path = SPECULAR_MAP;
731-
#[cfg(target_arch = "wasm32")]
732-
let city_path = CITY;
733-
734828
let city_size_units = LOCAL_CITY_BBOX_MAX_UNITS - LOCAL_CITY_BBOX_MIN_UNITS;
735829
let city_size_m = city_size_units * LOCAL_CITY_SCALE;
736830
let city_translation = Vec3::ZERO;
737831
let city_scale = Vec3::splat(LOCAL_CITY_SCALE);
738-
#[cfg(not(target_arch = "wasm32"))]
739-
let city_path_str = city_path.to_string_lossy().into_owned();
740-
#[cfg(target_arch = "wasm32")]
741-
let city_path_str = city_path.to_string();
742832
info!(
743833
"sim world: loading city {} (bbox {}x{}x{} units, scaled to {}x{}x{} m) with translation ({}, {}, {})",
744-
city_path_str,
834+
asset_paths.city.as_str(),
745835
city_size_units.x,
746836
city_size_units.y,
747837
city_size_units.z,
@@ -753,8 +843,8 @@ fn setup_world(
753843
city_translation.z
754844
);
755845

756-
let skybox_handle = asset_server.load(skybox_path);
757-
let specular_map_handle = asset_server.load(specular_map_path);
846+
let skybox_handle = asset_server.load(asset_paths.skybox.clone());
847+
let specular_map_handle = asset_server.load(asset_paths.specular_map.clone());
758848

759849
commands.insert_resource(GlobalAmbientLight {
760850
color: Color::WHITE,
@@ -821,14 +911,8 @@ fn setup_world(
821911
Transform::from_translation(Vec3::new(3.0, 10.0, 1.0)).looking_at(Vec3::ZERO, Vec3::Y),
822912
));
823913

824-
#[cfg(not(target_arch = "wasm32"))]
825-
let quadcopter_scene_path = format!("{}#scene0", quadcopter_path.display());
826-
#[cfg(target_arch = "wasm32")]
827-
let quadcopter_scene_path = format!("{quadcopter_path}#scene0");
828-
#[cfg(not(target_arch = "wasm32"))]
829-
let city_scene_path = format!("{}#scene0", city_path.display());
830-
#[cfg(target_arch = "wasm32")]
831-
let city_scene_path = format!("{city_path}#scene0");
914+
let quadcopter_scene_path = format!("{}#scene0", asset_paths.quadcopter.as_str());
915+
let city_scene_path = format!("{}#scene0", asset_paths.city.as_str());
832916

833917
let quadcopter_scene =
834918
asset_server.load(GltfAssetLabel::Scene(0).from_asset(quadcopter_scene_path));
@@ -2104,6 +2188,14 @@ fn sync_loading_overlay(
21042188
}
21052189

21062190
pub fn build_world(headless: bool, split_monitor: bool) -> App {
2191+
build_world_with_assets(headless, split_monitor, None)
2192+
}
2193+
2194+
fn build_world_with_assets(
2195+
headless: bool,
2196+
split_monitor: bool,
2197+
scene_assets: Option<SceneAssetPaths>,
2198+
) -> App {
21072199
let mut app = App::new();
21082200
app.insert_resource(SimState::default())
21092201
.insert_resource(SimMotorCommands::default())
@@ -2118,6 +2210,10 @@ pub fn build_world(headless: bool, split_monitor: bool) -> App {
21182210
.init_resource::<SceneLoadState>()
21192211
.init_resource::<SimHudSpawnState>();
21202212

2213+
if !headless {
2214+
app.insert_resource(scene_assets.unwrap_or_else(prepare_scene_assets));
2215+
}
2216+
21212217
if headless {
21222218
app.add_plugins(MinimalPlugins);
21232219
return app;
@@ -2181,7 +2277,8 @@ pub fn build_world(headless: bool, split_monitor: bool) -> App {
21812277

21822278
#[cfg(feature = "sim")]
21832279
pub fn run_sim() {
2184-
let mut app = build_world(false, false);
2280+
let scene_assets = prepare_scene_assets();
2281+
let mut app = build_world_with_assets(false, false, Some(scene_assets));
21852282
#[cfg(not(target_arch = "wasm32"))]
21862283
{
21872284
app.add_systems(Startup, setup_copper);
@@ -2198,9 +2295,10 @@ pub fn run_sim() {
21982295

21992296
#[cfg(feature = "bevymon")]
22002297
pub fn run_bevymon() {
2298+
let scene_assets = prepare_scene_assets();
22012299
let (monitor_model, copper) = build_bevymon_copper();
22022300

2203-
let mut app = build_world(false, true);
2301+
let mut app = build_world_with_assets(false, true, Some(scene_assets));
22042302
app.insert_resource(copper)
22052303
.init_resource::<LayoutSpawned>()
22062304
.add_plugins(

0 commit comments

Comments
 (0)