mirror of
https://github.com/bevyengine/bevy
synced 2024-11-21 20:23:28 +00:00
Bevy Asset V2 (#8624)
# Bevy Asset V2 Proposal ## Why Does Bevy Need A New Asset System? Asset pipelines are a central part of the gamedev process. Bevy's current asset system is missing a number of features that make it non-viable for many classes of gamedev. After plenty of discussions and [a long community feedback period](https://github.com/bevyengine/bevy/discussions/3972), we've identified a number missing features: * **Asset Preprocessing**: it should be possible to "preprocess" / "compile" / "crunch" assets at "development time" rather than when the game starts up. This enables offloading expensive work from deployed apps, faster asset loading, less runtime memory usage, etc. * **Per-Asset Loader Settings**: Individual assets cannot define their own loaders that override the defaults. Additionally, they cannot provide per-asset settings to their loaders. This is a huge limitation, as many asset types don't provide all information necessary for Bevy _inside_ the asset. For example, a raw PNG image says nothing about how it should be sampled (ex: linear vs nearest). * **Asset `.meta` files**: assets should have configuration files stored adjacent to the asset in question, which allows the user to configure asset-type-specific settings. These settings should be accessible during the pre-processing phase. Modifying a `.meta` file should trigger a re-processing / re-load of the asset. It should be possible to configure asset loaders from the meta file. * **Processed Asset Hot Reloading**: Changes to processed assets (or their dependencies) should result in re-processing them and re-loading the results in live Bevy Apps. * **Asset Dependency Tracking**: The current bevy_asset has no good way to wait for asset dependencies to load. It punts this as an exercise for consumers of the loader apis, which is unreasonable and error prone. There should be easy, ergonomic ways to wait for assets to load and block some logic on an asset's entire dependency tree loading. * **Runtime Asset Loading**: it should be (optionally) possible to load arbitrary assets dynamically at runtime. This necessitates being able to deploy and run the asset server alongside Bevy Apps on _all platforms_. For example, we should be able to invoke the shader compiler at runtime, stream scenes from sources like the internet, etc. To keep deployed binaries (and startup times) small, the runtime asset server configuration should be configurable with different settings compared to the "pre processor asset server". * **Multiple Backends**: It should be possible to load assets from arbitrary sources (filesystems, the internet, remote asset serves, etc). * **Asset Packing**: It should be possible to deploy assets in compressed "packs", which makes it easier and more efficient to distribute assets with Bevy Apps. * **Asset Handoff**: It should be possible to hold a "live" asset handle, which correlates to runtime data, without actually holding the asset in memory. Ex: it must be possible to hold a reference to a GPU mesh generated from a "mesh asset" without keeping the mesh data in CPU memory * **Per-Platform Processed Assets**: Different platforms and app distributions have different capabilities and requirements. Some platforms need lower asset resolutions or different asset formats to operate within the hardware constraints of the platform. It should be possible to define per-platform asset processing profiles. And it should be possible to deploy only the assets required for a given platform. These features have architectural implications that are significant enough to require a full rewrite. The current Bevy Asset implementation got us this far, but it can take us no farther. This PR defines a brand new asset system that implements most of these features, while laying the foundations for the remaining features to be built. ## Bevy Asset V2 Here is a quick overview of the features introduced in this PR. * **Asset Preprocessing**: Preprocess assets at development time into more efficient (and configurable) representations * **Dependency Aware**: Dependencies required to process an asset are tracked. If an asset's processed dependency changes, it will be reprocessed * **Hot Reprocessing/Reloading**: detect changes to asset source files, reprocess them if they have changed, and then hot-reload them in Bevy Apps. * **Only Process Changes**: Assets are only re-processed when their source file (or meta file) has changed. This uses hashing and timestamps to avoid processing assets that haven't changed. * **Transactional and Reliable**: Uses write-ahead logging (a technique commonly used by databases) to recover from crashes / forced-exits. Whenever possible it avoids full-reprocessing / only uncompleted transactions will be reprocessed. When the processor is running in parallel with a Bevy App, processor asset writes block Bevy App asset reads. Reading metadata + asset bytes is guaranteed to be transactional / correctly paired. * **Portable / Run anywhere / Database-free**: The processor does not rely on an in-memory database (although it uses some database techniques for reliability). This is important because pretty much all in-memory databases have unsupported platforms or build complications. * **Configure Processor Defaults Per File Type**: You can say "use this processor for all files of this type". * **Custom Processors**: The `Processor` trait is flexible and unopinionated. It can be implemented by downstream plugins. * **LoadAndSave Processors**: Most asset processing scenarios can be expressed as "run AssetLoader A, save the results using AssetSaver X, and then load the result using AssetLoader B". For example, load this png image using `PngImageLoader`, which produces an `Image` asset and then save it using `CompressedImageSaver` (which also produces an `Image` asset, but in a compressed format), which takes an `Image` asset as input. This means if you have an `AssetLoader` for an asset, you are already half way there! It also means that you can share AssetSavers across multiple loaders. Because `CompressedImageSaver` accepts Bevy's generic Image asset as input, it means you can also use it with some future `JpegImageLoader`. * **Loader and Saver Settings**: Asset Loaders and Savers can now define their own settings types, which are passed in as input when an asset is loaded / saved. Each asset can define its own settings. * **Asset `.meta` files**: configure asset loaders, their settings, enable/disable processing, and configure processor settings * **Runtime Asset Dependency Tracking** Runtime asset dependencies (ex: if an asset contains a `Handle<Image>`) are tracked by the asset server. An event is emitted when an asset and all of its dependencies have been loaded * **Unprocessed Asset Loading**: Assets do not require preprocessing. They can be loaded directly. A processed asset is just a "normal" asset with some extra metadata. Asset Loaders don't need to know or care about whether or not an asset was processed. * **Async Asset IO**: Asset readers/writers use async non-blocking interfaces. Note that because Rust doesn't yet support async traits, there is a bit of manual Boxing / Future boilerplate. This will hopefully be removed in the near future when Rust gets async traits. * **Pluggable Asset Readers and Writers**: Arbitrary asset source readers/writers are supported, both by the processor and the asset server. * **Better Asset Handles** * **Single Arc Tree**: Asset Handles now use a single arc tree that represents the lifetime of the asset. This makes their implementation simpler, more efficient, and allows us to cheaply attach metadata to handles. Ex: the AssetPath of a handle is now directly accessible on the handle itself! * **Const Typed Handles**: typed handles can be constructed in a const context. No more weird "const untyped converted to typed at runtime" patterns! * **Handles and Ids are Smaller / Faster To Hash / Compare**: Typed `Handle<T>` is now much smaller in memory and `AssetId<T>` is even smaller. * **Weak Handle Usage Reduction**: In general Handles are now considered to be "strong". Bevy features that previously used "weak `Handle<T>`" have been ported to `AssetId<T>`, which makes it statically clear that the features do not hold strong handles (while retaining strong type information). Currently Handle::Weak still exists, but it is very possible that we can remove that entirely. * **Efficient / Dense Asset Ids**: Assets now have efficient dense runtime asset ids, which means we can avoid expensive hash lookups. Assets are stored in Vecs instead of HashMaps. There are now typed and untyped ids, which means we no longer need to store dynamic type information in the ID for typed handles. "AssetPathId" (which was a nightmare from a performance and correctness standpoint) has been entirely removed in favor of dense ids (which are retrieved for a path on load) * **Direct Asset Loading, with Dependency Tracking**: Assets that are defined at runtime can still have their dependencies tracked by the Asset Server (ex: if you create a material at runtime, you can still wait for its textures to load). This is accomplished via the (currently optional) "asset dependency visitor" trait. This system can also be used to define a set of assets to load, then wait for those assets to load. * **Async folder loading**: Folder loading also uses this system and immediately returns a handle to the LoadedFolder asset, which means folder loading no longer blocks on directory traversals. * **Improved Loader Interface**: Loaders now have a specific "top level asset type", which makes returning the top-level asset simpler and statically typed. * **Basic Image Settings and Processing**: Image assets can now be processed into the gpu-friendly Basic Universal format. The ImageLoader now has a setting to define what format the image should be loaded as. Note that this is just a minimal MVP ... plenty of additional work to do here. To demo this, enable the `basis-universal` feature and turn on asset processing. * **Simpler Audio Play / AudioSink API**: Asset handle providers are cloneable, which means the Audio resource can mint its own handles. This means you can now do `let sink_handle = audio.play(music)` instead of `let sink_handle = audio_sinks.get_handle(audio.play(music))`. Note that this might still be replaced by https://github.com/bevyengine/bevy/pull/8424. **Removed Handle Casting From Engine Features**: Ex: FontAtlases no longer use casting between handle types ## Using The New Asset System ### Normal Unprocessed Asset Loading By default the `AssetPlugin` does not use processing. It behaves pretty much the same way as the old system. If you are defining a custom asset, first derive `Asset`: ```rust #[derive(Asset)] struct Thing { value: String, } ``` Initialize the asset: ```rust app.init_asset:<Thing>() ``` Implement a new `AssetLoader` for it: ```rust #[derive(Default)] struct ThingLoader; #[derive(Serialize, Deserialize, Default)] pub struct ThingSettings { some_setting: bool, } impl AssetLoader for ThingLoader { type Asset = Thing; type Settings = ThingSettings; fn load<'a>( &'a self, reader: &'a mut Reader, settings: &'a ThingSettings, load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result<Thing, anyhow::Error>> { Box::pin(async move { let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; // convert bytes to value somehow Ok(Thing { value }) }) } fn extensions(&self) -> &[&str] { &["thing"] } } ``` Note that this interface will get much cleaner once Rust gets support for async traits. `Reader` is an async futures_io::AsyncRead. You can stream bytes as they come in or read them all into a `Vec<u8>`, depending on the context. You can use `let handle = load_context.load(path)` to kick off a dependency load, retrieve a handle, and register the dependency for the asset. Then just register the loader in your Bevy app: ```rust app.init_asset_loader::<ThingLoader>() ``` Now just add your `Thing` asset files into the `assets` folder and load them like this: ```rust fn system(asset_server: Res<AssetServer>) { let handle = Handle<Thing> = asset_server.load("cool.thing"); } ``` You can check load states directly via the asset server: ```rust if asset_server.load_state(&handle) == LoadState::Loaded { } ``` You can also listen for events: ```rust fn system(mut events: EventReader<AssetEvent<Thing>>, handle: Res<SomeThingHandle>) { for event in events.iter() { if event.is_loaded_with_dependencies(&handle) { } } } ``` Note the new `AssetEvent::LoadedWithDependencies`, which only fires when the asset is loaded _and_ all dependencies (and their dependencies) have loaded. Unlike the old asset system, for a given asset path all `Handle<T>` values point to the same underlying Arc. This means Handles can cheaply hold more asset information, such as the AssetPath: ```rust // prints the AssetPath of the handle info!("{:?}", handle.path()) ``` ### Processed Assets Asset processing can be enabled via the `AssetPlugin`. When developing Bevy Apps with processed assets, do this: ```rust app.add_plugins(DefaultPlugins.set(AssetPlugin::processed_dev())) ``` This runs the `AssetProcessor` in the background with hot-reloading. It reads assets from the `assets` folder, processes them, and writes them to the `.imported_assets` folder. Asset loads in the Bevy App will wait for a processed version of the asset to become available. If an asset in the `assets` folder changes, it will be reprocessed and hot-reloaded in the Bevy App. When deploying processed Bevy apps, do this: ```rust app.add_plugins(DefaultPlugins.set(AssetPlugin::processed())) ``` This does not run the `AssetProcessor` in the background. It behaves like `AssetPlugin::unprocessed()`, but reads assets from `.imported_assets`. When the `AssetProcessor` is running, it will populate sibling `.meta` files for assets in the `assets` folder. Meta files for assets that do not have a processor configured look like this: ```rust ( meta_format_version: "1.0", asset: Load( loader: "bevy_render::texture::image_loader::ImageLoader", settings: ( format: FromExtension, ), ), ) ``` This is metadata for an image asset. For example, if you have `assets/my_sprite.png`, this could be the metadata stored at `assets/my_sprite.png.meta`. Meta files are totally optional. If no metadata exists, the default settings will be used. In short, this file says "load this asset with the ImageLoader and use the file extension to determine the image type". This type of meta file is supported in all AssetPlugin modes. If in `Unprocessed` mode, the asset (with the meta settings) will be loaded directly. If in `ProcessedDev` mode, the asset file will be copied directly to the `.imported_assets` folder. The meta will also be copied directly to the `.imported_assets` folder, but with one addition: ```rust ( meta_format_version: "1.0", processed_info: Some(( hash: 12415480888597742505, full_hash: 14344495437905856884, process_dependencies: [], )), asset: Load( loader: "bevy_render::texture::image_loader::ImageLoader", settings: ( format: FromExtension, ), ), ) ``` `processed_info` contains `hash` (a direct hash of the asset and meta bytes), `full_hash` (a hash of `hash` and the hashes of all `process_dependencies`), and `process_dependencies` (the `path` and `full_hash` of every process_dependency). A "process dependency" is an asset dependency that is _directly_ used when processing the asset. Images do not have process dependencies, so this is empty. When the processor is enabled, you can use the `Process` metadata config: ```rust ( meta_format_version: "1.0", asset: Process( processor: "bevy_asset::processor::process::LoadAndSave<bevy_render::texture::image_loader::ImageLoader, bevy_render::texture::compressed_image_saver::CompressedImageSaver>", settings: ( loader_settings: ( format: FromExtension, ), saver_settings: ( generate_mipmaps: true, ), ), ), ) ``` This configures the asset to use the `LoadAndSave` processor, which runs an AssetLoader and feeds the result into an AssetSaver (which saves the given Asset and defines a loader to load it with). (for terseness LoadAndSave will likely get a shorter/friendlier type name when [Stable Type Paths](#7184) lands). `LoadAndSave` is likely to be the most common processor type, but arbitrary processors are supported. `CompressedImageSaver` saves an `Image` in the Basis Universal format and configures the ImageLoader to load it as basis universal. The `AssetProcessor` will read this meta, run it through the LoadAndSave processor, and write the basis-universal version of the image to `.imported_assets`. The final metadata will look like this: ```rust ( meta_format_version: "1.0", processed_info: Some(( hash: 905599590923828066, full_hash: 9948823010183819117, process_dependencies: [], )), asset: Load( loader: "bevy_render::texture::image_loader::ImageLoader", settings: ( format: Format(Basis), ), ), ) ``` To try basis-universal processing out in Bevy examples, (for example `sprite.rs`), change `add_plugins(DefaultPlugins)` to `add_plugins(DefaultPlugins.set(AssetPlugin::processed_dev()))` and run with the `basis-universal` feature enabled: `cargo run --features=basis-universal --example sprite`. To create a custom processor, there are two main paths: 1. Use the `LoadAndSave` processor with an existing `AssetLoader`. Implement the `AssetSaver` trait, register the processor using `asset_processor.register_processor::<LoadAndSave<ImageLoader, CompressedImageSaver>>(image_saver.into())`. 2. Implement the `Process` trait directly and register it using: `asset_processor.register_processor(thing_processor)`. You can configure default processors for file extensions like this: ```rust asset_processor.set_default_processor::<ThingProcessor>("thing") ``` There is one more metadata type to be aware of: ```rust ( meta_format_version: "1.0", asset: Ignore, ) ``` This will ignore the asset during processing / prevent it from being written to `.imported_assets`. The AssetProcessor stores a transaction log at `.imported_assets/log` and uses it to gracefully recover from unexpected stops. This means you can force-quit the processor (and Bevy Apps running the processor in parallel) at arbitrary times! `.imported_assets` is "local state". It should _not_ be checked into source control. It should also be considered "read only". In practice, you _can_ modify processed assets and processed metadata if you really need to test something. But those modifications will not be represented in the hashes of the assets, so the processed state will be "out of sync" with the source assets. The processor _will not_ fix this for you. Either revert the change after you have tested it, or delete the processed files so they can be re-populated. ## Open Questions There are a number of open questions to be discussed. We should decide if they need to be addressed in this PR and if so, how we will address them: ### Implied Dependencies vs Dependency Enumeration There are currently two ways to populate asset dependencies: * **Implied via AssetLoaders**: if an AssetLoader loads an asset (and retrieves a handle), a dependency is added to the list. * **Explicit via the optional Asset::visit_dependencies**: if `server.load_asset(my_asset)` is called, it will call `my_asset.visit_dependencies`, which will grab dependencies that have been manually defined for the asset via the Asset trait impl (which can be derived). This means that defining explicit dependencies is optional for "loaded assets". And the list of dependencies is always accurate because loaders can only produce Handles if they register dependencies. If an asset was loaded with an AssetLoader, it only uses the implied dependencies. If an asset was created at runtime and added with `asset_server.load_asset(MyAsset)`, it will use `Asset::visit_dependencies`. However this can create a behavior mismatch between loaded assets and equivalent "created at runtime" assets if `Assets::visit_dependencies` doesn't exactly match the dependencies produced by the AssetLoader. This behavior mismatch can be resolved by completely removing "implied loader dependencies" and requiring `Asset::visit_dependencies` to supply dependency data. But this creates two problems: * It makes defining loaded assets harder and more error prone: Devs must remember to manually annotate asset dependencies with `#[dependency]` when deriving `Asset`. For more complicated assets (such as scenes), the derive likely wouldn't be sufficient and a manual `visit_dependencies` impl would be required. * Removes the ability to immediately kick off dependency loads: When AssetLoaders retrieve a Handle, they also immediately kick off an asset load for the handle, which means it can start loading in parallel _before_ the asset finishes loading. For large assets, this could be significant. (although this could be mitigated for processed assets if we store dependencies in the processed meta file and load them ahead of time) ### Eager ProcessorDev Asset Loading I made a controversial call in the interest of fast startup times ("time to first pixel") for the "processor dev mode configuration". When initializing the AssetProcessor, current processed versions of unchanged assets are yielded immediately, even if their dependencies haven't been checked yet for reprocessing. This means that non-current-state-of-filesystem-but-previously-valid assets might be returned to the App first, then hot-reloaded if/when their dependencies change and the asset is reprocessed. Is this behavior desirable? There is largely one alternative: do not yield an asset from the processor to the app until all of its dependencies have been checked for changes. In some common cases (load dependency has not changed since last run) this will increase startup time. The main question is "by how much" and is that slower startup time worth it in the interest of only yielding assets that are true to the current state of the filesystem. Should this be configurable? I'm starting to think we should only yield an asset after its (historical) dependencies have been checked for changes + processed as necessary, but I'm curious what you all think. ### Paths Are Currently The Only Canonical ID / Do We Want Asset UUIDs? In this implementation AssetPaths are the only canonical asset identifier (just like the previous Bevy Asset system and Godot). Moving assets will result in re-scans (and currently reprocessing, although reprocessing can easily be avoided with some changes). Asset renames/moves will break code and assets that rely on specific paths, unless those paths are fixed up. Do we want / need "stable asset uuids"? Introducing them is very possible: 1. Generate a UUID and include it in .meta files 2. Support UUID in AssetPath 3. Generate "asset indices" which are loaded on startup and map UUIDs to paths. 4 (maybe). Consider only supporting UUIDs for processed assets so we can generate quick-to-load indices instead of scanning meta files. The main "pro" is that assets referencing UUIDs don't need to be migrated when a path changes. The main "con" is that UUIDs cannot be "lazily resolved" like paths. They need a full view of all assets to answer the question "does this UUID exist". Which means UUIDs require the AssetProcessor to fully finish startup scans before saying an asset doesnt exist. And they essentially require asset pre-processing to use in apps, because scanning all asset metadata files at runtime to resolve a UUID is not viable for medium-to-large apps. It really requires a pre-generated UUID index, which must be loaded before querying for assets. I personally think this should be investigated in a separate PR. Paths aren't going anywhere ... _everyone_ uses filesystems (and filesystem-like apis) to manage their asset source files. I consider them permanent canonical asset information. Additionally, they behave well for both processed and unprocessed asset modes. Given that Bevy is supporting both, this feels like the right canonical ID to start with. UUIDS (and maybe even other indexed-identifier types) can be added later as necessary. ### Folder / File Naming Conventions All asset processing config currently lives in the `.imported_assets` folder. The processor transaction log is in `.imported_assets/log`. Processed assets are added to `.imported_assets/Default`, which will make migrating to processed asset profiles (ex: a `.imported_assets/Mobile` profile) a non-breaking change. It also allows us to create top-level files like `.imported_assets/log` without it being interpreted as an asset. Meta files currently have a `.meta` suffix. Do we like these names and conventions? ### Should the `AssetPlugin::processed_dev` configuration enable `watch_for_changes` automatically? Currently it does (which I think makes sense), but it does make it the only configuration that enables watch_for_changes by default. ### Discuss on_loaded High Level Interface: This PR includes a very rough "proof of concept" `on_loaded` system adapter that uses the `LoadedWithDependencies` event in combination with `asset_server.load_asset` dependency tracking to support this pattern ```rust fn main() { App::new() .init_asset::<MyAssets>() .add_systems(Update, on_loaded(create_array_texture)) .run(); } #[derive(Asset, Clone)] struct MyAssets { #[dependency] picture_of_my_cat: Handle<Image>, #[dependency] picture_of_my_other_cat: Handle<Image>, } impl FromWorld for ArrayTexture { fn from_world(world: &mut World) -> Self { picture_of_my_cat: server.load("meow.png"), picture_of_my_other_cat: server.load("meeeeeeeow.png"), } } fn spawn_cat(In(my_assets): In<MyAssets>, mut commands: Commands) { commands.spawn(SpriteBundle { texture: my_assets.picture_of_my_cat.clone(), ..default() }); commands.spawn(SpriteBundle { texture: my_assets.picture_of_my_other_cat.clone(), ..default() }); } ``` The implementation is _very_ rough. And it is currently unsafe because `bevy_ecs` doesn't expose some internals to do this safely from inside `bevy_asset`. There are plenty of unanswered questions like: * "do we add a Loadable" derive? (effectively automate the FromWorld implementation above) * Should `MyAssets` even be an Asset? (largely implemented this way because it elegantly builds on `server.load_asset(MyAsset { .. })` dependency tracking). We should think hard about what our ideal API looks like (and if this is a pattern we want to support). Not necessarily something we need to solve in this PR. The current `on_loaded` impl should probably be removed from this PR before merging. ## Clarifying Questions ### What about Assets as Entities? This Bevy Asset V2 proposal implementation initially stored Assets as ECS Entities. Instead of `AssetId<T>` + the `Assets<T>` resource it used `Entity` as the asset id and Asset values were just ECS components. There are plenty of compelling reasons to do this: 1. Easier to inline assets in Bevy Scenes (as they are "just" normal entities + components) 2. More flexible queries: use the power of the ECS to filter assets (ex: `Query<Mesh, With<Tree>>`). 3. Extensible. Users can add arbitrary component data to assets. 4. Things like "component visualization tools" work out of the box to visualize asset data. However Assets as Entities has a ton of caveats right now: * We need to be able to allocate entity ids without a direct World reference (aka rework id allocator in Entities ... i worked around this in my prototypes by just pre allocating big chunks of entities) * We want asset change events in addition to ECS change tracking ... how do we populate them when mutations can come from anywhere? Do we use Changed queries? This would require iterating over the change data for all assets every frame. Is this acceptable or should we implement a new "event based" component change detection option? * Reconciling manually created assets with asset-system managed assets has some nuance (ex: are they "loaded" / do they also have that component metadata?) * "how do we handle "static" / default entity handles" (ties in to the Entity Indices discussion: https://github.com/bevyengine/bevy/discussions/8319). This is necessary for things like "built in" assets and default handles in things like SpriteBundle. * Storing asset information as a component makes it easy to "invalidate" asset state by removing the component (or forcing modifications). Ideally we have ways to lock this down (some combination of Rust type privacy and ECS validation) In practice, how we store and identify assets is a reasonably superficial change (porting off of Assets as Entities and implementing dedicated storage + ids took less than a day). So once we sort out the remaining challenges the flip should be straightforward. Additionally, I do still have "Assets as Entities" in my commit history, so we can reuse that work. I personally think "assets as entities" is a good endgame, but it also doesn't provide _significant_ value at the moment and it certainly isn't ready yet with the current state of things. ### Why not Distill? [Distill](https://github.com/amethyst/distill) is a high quality fully featured asset system built in Rust. It is very natural to ask "why not just use Distill?". It is also worth calling out that for awhile, [we planned on adopting Distill / I signed off on it](https://github.com/bevyengine/bevy/issues/708). However I think Bevy has a number of constraints that make Distill adoption suboptimal: * **Architectural Simplicity:** * Distill's processor requires an in-memory database (lmdb) and RPC networked API (using Cap'n Proto). Each of these introduces API complexity that increases maintenance burden and "code grokability". Ignoring tests, documentation, and examples, Distill has 24,237 lines of Rust code (including generated code for RPC + database interactions). If you ignore generated code, it has 11,499 lines. * Bevy builds the AssetProcessor and AssetServer using pluggable AssetReader/AssetWriter Rust traits with simple io interfaces. They do not necessitate databases or RPC interfaces (although Readers/Writers could use them if that is desired). Bevy Asset V2 (at the time of writing this PR) is 5,384 lines of Rust code (ignoring tests, documentation, and examples). Grain of salt: Distill does have more features currently (ex: Asset Packing, GUIDS, remote-out-of-process asset processor). I do plan to implement these features in Bevy Asset V2 and I personally highly doubt they will meaningfully close the 6115 lines-of-code gap. * This complexity gap (which while illustrated by lines of code, is much bigger than just that) is noteworthy to me. Bevy should be hackable and there are pillars of Distill that are very hard to understand and extend. This is a matter of opinion (and Bevy Asset V2 also has complicated areas), but I think Bevy Asset V2 is much more approachable for the average developer. * Necessary disclaimer: counting lines of code is an extremely rough complexity metric. Read the code and form your own opinions. * **Optional Asset Processing:** Not all Bevy Apps (or Bevy App developers) need / want asset preprocessing. Processing increases the complexity of the development environment by introducing things like meta files, imported asset storage, running processors in the background, waiting for processing to finish, etc. Distill _requires_ preprocessing to work. With Bevy Asset V2 processing is fully opt-in. The AssetServer isn't directly aware of asset processors at all. AssetLoaders only care about converting bytes to runtime Assets ... they don't know or care if the bytes were pre-processed or not. Processing is "elegantly" (forgive my self-congratulatory phrasing) layered on top and builds on the existing Asset system primitives. * **Direct Filesystem Access to Processed Asset State:** Distill stores processed assets in a database. This makes debugging / inspecting the processed outputs harder (either requires special tooling to query the database or they need to be "deployed" to be inspected). Bevy Asset V2, on the other hand, stores processed assets in the filesystem (by default ... this is configurable). This makes interacting with the processed state more natural. Note that both Godot and Unity's new asset system store processed assets in the filesystem. * **Portability**: Because Distill's processor uses lmdb and RPC networking, it cannot be run on certain platforms (ex: lmdb is a non-rust dependency that cannot run on the web, some platforms don't support running network servers). Bevy should be able to process assets everywhere (ex: run the Bevy Editor on the web, compile + process shaders on mobile, etc). Distill does partially mitigate this problem by supporting "streaming" assets via the RPC protocol, but this is not a full solve from my perspective. And Bevy Asset V2 can (in theory) also stream assets (without requiring RPC, although this isn't implemented yet) Note that I _do_ still think Distill would be a solid asset system for Bevy. But I think the approach in this PR is a better solve for Bevy's specific "asset system requirements". ### Doesn't async-fs just shim requests to "sync" `std::fs`? What is the point? "True async file io" has limited / spotty platform support. async-fs (and the rust async ecosystem generally ... ex Tokio) currently use async wrappers over std::fs that offload blocking requests to separate threads. This may feel unsatisfying, but it _does_ still provide value because it prevents our task pools from blocking on file system operations (which would prevent progress when there are many tasks to do, but all threads in a pool are currently blocking on file system ops). Additionally, using async APIs for our AssetReaders and AssetWriters also provides value because we can later add support for "true async file io" for platforms that support it. _And_ we can implement other "true async io" asset backends (such as networked asset io). ## Draft TODO - [x] Fill in missing filesystem event APIs: file removed event (which is expressed as dangling RenameFrom events in some cases), file/folder renamed event - [x] Assets without loaders are not moved to the processed folder. This breaks things like referenced `.bin` files for GLTFs. This should be configurable per-non-asset-type. - [x] Initial implementation of Reflect and FromReflect for Handle. The "deserialization" parity bar is low here as this only worked with static UUIDs in the old impl ... this is a non-trivial problem. Either we add a Handle::AssetPath variant that gets "upgraded" to a strong handle on scene load or we use a separate AssetRef type for Bevy scenes (which is converted to a runtime Handle on load). This deserves its own discussion in a different pr. - [x] Populate read_asset_bytes hash when run by the processor (a bit of a special case .. when run by the processor the processed meta will contain the hash so we don't need to compute it on the spot, but we don't want/need to read the meta when run by the main AssetServer) - [x] Delay hot reloading: currently filesystem events are handled immediately, which creates timing issues in some cases. For example hot reloading images can sometimes break because the image isn't finished writing. We should add a delay, likely similar to the [implementation in this PR](https://github.com/bevyengine/bevy/pull/8503). - [x] Port old platform-specific AssetIo implementations to the new AssetReader interface (currently missing Android and web) - [x] Resolve on_loaded unsafety (either by removing the API entirely or removing the unsafe) - [x] Runtime loader setting overrides - [x] Remove remaining unwraps that should be error-handled. There are number of TODOs here - [x] Pretty AssetPath Display impl - [x] Document more APIs - [x] Resolve spurious "reloading because it has changed" events (to repro run load_gltf with `processed_dev()`) - [x] load_dependency hot reloading currently only works for processed assets. If processing is disabled, load_dependency changes are not hot reloaded. - [x] Replace AssetInfo dependency load/fail counters with `loading_dependencies: HashSet<UntypedAssetId>` to prevent reloads from (potentially) breaking counters. Storing this will also enable "dependency reloaded" events (see [Next Steps](#next-steps)) - [x] Re-add filesystem watcher cargo feature gate (currently it is not optional) - [ ] Migration Guide - [ ] Changelog ## Followup TODO - [ ] Replace "eager unchanged processed asset loading" behavior with "don't returned unchanged processed asset until dependencies have been checked". - [ ] Add true `Ignore` AssetAction that does not copy the asset to the imported_assets folder. - [ ] Finish "live asset unloading" (ex: free up CPU asset memory after uploading an image to the GPU), rethink RenderAssets, and port renderer features. The `Assets` collection uses `Option<T>` for asset storage to support its removal. (1) the Option might not actually be necessary ... might be able to just remove from the collection entirely (2) need to finalize removal apis - [ ] Try replacing the "channel based" asset id recycling with something a bit more efficient (ex: we might be able to use raw atomic ints with some cleverness) - [ ] Consider adding UUIDs to processed assets (scoped just to helping identify moved assets ... not exposed to load queries ... see [Next Steps](#next-steps)) - [ ] Store "last modified" source asset and meta timestamps in processed meta files to enable skipping expensive hashing when the file wasn't changed - [ ] Fix "slow loop" handle drop fix - [ ] Migrate to TypeName - [x] Handle "loader preregistration". See #9429 ## Next Steps * **Configurable per-type defaults for AssetMeta**: It should be possible to add configuration like "all png image meta should default to using nearest sampling" (currently this hard-coded per-loader/processor Settings::default() impls). Also see the "Folder Meta" bullet point. * **Avoid Reprocessing on Asset Renames / Moves**: See the "canonical asset ids" discussion in [Open Questions](#open-questions) and the relevant bullet point in [Draft TODO](#draft-todo). Even without canonical ids, folder renames could avoid reprocessing in some cases. * **Multiple Asset Sources**: Expand AssetPath to support "asset source names" and support multiple AssetReaders in the asset server (ex: `webserver://some_path/image.png` backed by an Http webserver AssetReader). The "default" asset reader would use normal `some_path/image.png` paths. Ideally this works in combination with multiple AssetWatchers for hot-reloading * **Stable Type Names**: this pr removes the TypeUuid requirement from assets in favor of `std::any::type_name`. This makes defining assets easier (no need to generate a new uuid / use weird proc macro syntax). It also makes reading meta files easier (because things have "friendly names"). We also use type names for components in scene files. If they are good enough for components, they are good enough for assets. And consistency across Bevy pillars is desirable. However, `std::any::type_name` is not guaranteed to be stable (although in practice it is). We've developed a [stable type path](https://github.com/bevyengine/bevy/pull/7184) to resolve this, which should be adopted when it is ready. * **Command Line Interface**: It should be possible to run the asset processor in a separate process from the command line. This will also require building a network-server-backed AssetReader to communicate between the app and the processor. We've been planning to build a "bevy cli" for awhile. This seems like a good excuse to build it. * **Asset Packing**: This is largely an additive feature, so it made sense to me to punt this until we've laid the foundations in this PR. * **Per-Platform Processed Assets**: It should be possible to generate assets for multiple platforms by supporting multiple "processor profiles" per asset (ex: compress with format X on PC and Y on iOS). I think there should probably be arbitrary "profiles" (which can be separate from actual platforms), which are then assigned to a given platform when generating the final asset distribution for that platform. Ex: maybe devs want a "Mobile" profile that is shared between iOS and Android. Or a "LowEnd" profile shared between web and mobile. * **Versioning and Migrations**: Assets, Loaders, Savers, and Processors need to have versions to determine if their schema is valid. If an asset / loader version is incompatible with the current version expected at runtime, the processor should be able to migrate them. I think we should try using Bevy Reflect for this, as it would allow us to load the old version as a dynamic Reflect type without actually having the old Rust type. It would also allow us to define "patches" to migrate between versions (Bevy Reflect devs are currently working on patching). The `.meta` file already has its own format version. Migrating that to new versions should also be possible. * **Real Copy-on-write AssetPaths**: Rust's actual Cow (clone-on-write type) currently used by AssetPath can still result in String clones that aren't actually necessary (cloning an Owned Cow clones the contents). Bevy's asset system requires cloning AssetPaths in a number of places, which result in actual clones of the internal Strings. This is not efficient. AssetPath internals should be reworked to exhibit truer cow-like-behavior that reduces String clones to the absolute minimum. * **Consider processor-less processing**: In theory the AssetServer could run processors "inline" even if the background AssetProcessor is disabled. If we decide this is actually desirable, we could add this. But I don't think its a priority in the short or medium term. * **Pre-emptive dependency loading**: We could encode dependencies in processed meta files, which could then be used by the Asset Server to kick of dependency loads as early as possible (prior to starting the actual asset load). Is this desirable? How much time would this save in practice? * **Optimize Processor With UntypedAssetIds**: The processor exclusively uses AssetPath to identify assets currently. It might be possible to swap these out for UntypedAssetIds in some places, which are smaller / cheaper to hash and compare. * **One to Many Asset Processing**: An asset source file that produces many assets currently must be processed into a single "processed" asset source. If labeled assets can be written separately they can each have their own configured savers _and_ they could be loaded more granularly. Definitely worth exploring! * **Automatically Track "Runtime-only" Asset Dependencies**: Right now, tracking "created at runtime" asset dependencies requires adding them via `asset_server.load_asset(StandardMaterial::default())`. I think with some cleverness we could also do this for `materials.add(StandardMaterial::default())`, making tracking work "everywhere". There are challenges here relating to change detection / ensuring the server is made aware of dependency changes. This could be expensive in some cases. * **"Dependency Changed" events**: Some assets have runtime artifacts that need to be re-generated when one of their dependencies change (ex: regenerate a material's bind group when a Texture needs to change). We are generating the dependency graph so we can definitely produce these events. Buuuuut generating these events will have a cost / they could be high frequency for some assets, so we might want this to be opt-in for specific cases. * **Investigate Storing More Information In Handles**: Handles can now store arbitrary information, which makes it cheaper and easier to access. How much should we move into them? Canonical asset load states (via atomics)? (`handle.is_loaded()` would be very cool). Should we store the entire asset and remove the `Assets<T>` collection? (`Arc<RwLock<Option<Image>>>`?) * **Support processing and loading files without extensions**: This is a pretty arbitrary restriction and could be supported with very minimal changes. * **Folder Meta**: It would be nice if we could define per folder processor configuration defaults (likely in a `.meta` or `.folder_meta` file). Things like "default to linear filtering for all Images in this folder". * **Replace async_broadcast with event-listener?** This might be approximately drop-in for some uses and it feels more light weight * **Support Running the AssetProcessor on the Web**: Most of the hard work is done here, but there are some easy straggling TODOs (make the transaction log an interface instead of a direct file writer so we can write a web storage backend, implement an AssetReader/AssetWriter that reads/writes to something like LocalStorage). * **Consider identifying and preventing circular dependencies**: This is especially important for "processor dependencies", as processing will silently never finish in these cases. * **Built-in/Inlined Asset Hot Reloading**: This PR regresses "built-in/inlined" asset hot reloading (previously provided by the DebugAssetServer). I'm intentionally punting this because I think it can be cleanly implemented with "multiple asset sources" by registering a "debug asset source" (ex: `debug://bevy_pbr/src/render/pbr.wgsl` asset paths) in combination with an AssetWatcher for that asset source and support for "manually loading pats with asset bytes instead of AssetReaders". The old DebugAssetServer was quite nasty and I'd love to avoid that hackery going forward. * **Investigate ways to remove double-parsing meta files**: Parsing meta files currently involves parsing once with "minimal" versions of the meta file to extract the type name of the loader/processor config, then parsing again to parse the "full" meta. This is suboptimal. We should be able to define custom deserializers that (1) assume the loader/processor type name comes first (2) dynamically looks up the loader/processor registrations to deserialize settings in-line (similar to components in the bevy scene format). Another alternative: deserialize as dynamic Reflect objects and then convert. * **More runtime loading configuration**: Support using the Handle type as a hint to select an asset loader (instead of relying on AssetPath extensions) * **More high level Processor trait implementations**: For example, it might be worth adding support for arbitrary chains of "asset transforms" that modify an in-memory asset representation between loading and saving. (ex: load a Mesh, run a `subdivide_mesh` transform, followed by a `flip_normals` transform, then save the mesh to an efficient compressed format). * **Bevy Scene Handle Deserialization**: (see the relevant [Draft TODO item](#draft-todo) for context) * **Explore High Level Load Interfaces**: See [this discussion](#discuss-on_loaded-high-level-interface) for one prototype. * **Asset Streaming**: It would be great if we could stream Assets (ex: stream a long video file piece by piece) * **ID Exchanging**: In this PR Asset Handles/AssetIds are bigger than they need to be because they have a Uuid enum variant. If we implement an "id exchanging" system that trades Uuids for "efficient runtime ids", we can cut down on the size of AssetIds, making them more efficient. This has some open design questions, such as how to spawn entities with "default" handle values (as these wouldn't have access to the exchange api in the current system). * **Asset Path Fixup Tooling**: Assets that inline asset paths inside them will break when an asset moves. The asset system provides the functionality to detect when paths break. We should build a framework that enables formats to define "path migrations". This is especially important for scene files. For editor-generated files, we should also consider using UUIDs (see other bullet point) to avoid the need to migrate in these cases. --------- Co-authored-by: BeastLe9enD <beastle9end@outlook.de> Co-authored-by: Mike <mike.hsu@gmail.com> Co-authored-by: Nicola Papale <nicopap@users.noreply.github.com>
This commit is contained in:
parent
6c1f4668c7
commit
5eb292dc10
168 changed files with 10062 additions and 4668 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -12,3 +12,7 @@ dxil.dll
|
|||
|
||||
# Generated by "examples/scene/scene.rs"
|
||||
assets/scenes/load_scene_example-new.scn.ron
|
||||
|
||||
assets/**/*.meta
|
||||
crates/bevy_asset/imported_assets
|
||||
imported_assets
|
||||
|
|
30
Cargo.toml
30
Cargo.toml
|
@ -51,7 +51,6 @@ default = [
|
|||
"zstd",
|
||||
"vorbis",
|
||||
"x11",
|
||||
"filesystem_watcher",
|
||||
"bevy_gizmos",
|
||||
"android_shared_stdcxx",
|
||||
"tonemapping_luts",
|
||||
|
@ -194,9 +193,6 @@ symphonia-vorbis = ["bevy_internal/symphonia-vorbis"]
|
|||
# WAV audio format support (through symphonia)
|
||||
symphonia-wav = ["bevy_internal/symphonia-wav"]
|
||||
|
||||
# Enable watching file system for asset hot reload
|
||||
filesystem_watcher = ["bevy_internal/filesystem_watcher"]
|
||||
|
||||
# Enable serialization support through serde
|
||||
serialize = ["bevy_internal/serialize"]
|
||||
|
||||
|
@ -215,9 +211,6 @@ subpixel_glyph_atlas = ["bevy_internal/subpixel_glyph_atlas"]
|
|||
# Enable systems that allow for automated testing on CI
|
||||
bevy_ci_testing = ["bevy_internal/bevy_ci_testing"]
|
||||
|
||||
# Enable the "debug asset server" for hot reloading internal assets
|
||||
debug_asset_server = ["bevy_internal/debug_asset_server"]
|
||||
|
||||
# Enable animation support, and glTF animation loading
|
||||
animation = ["bevy_internal/animation", "bevy_animation"]
|
||||
|
||||
|
@ -248,6 +241,9 @@ shader_format_spirv = ["bevy_internal/shader_format_spirv"]
|
|||
# Enable some limitations to be able to use WebGL2. If not enabled, it will default to WebGPU in Wasm
|
||||
webgl2 = ["bevy_internal/webgl"]
|
||||
|
||||
# Enables watching the filesystem for Bevy Asset hot-reloading
|
||||
filesystem_watcher = ["bevy_internal/filesystem_watcher"]
|
||||
|
||||
[dependencies]
|
||||
bevy_dylib = { path = "crates/bevy_dylib", version = "0.12.0-dev", default-features = false, optional = true }
|
||||
bevy_internal = { path = "crates/bevy_internal", version = "0.12.0-dev", default-features = false }
|
||||
|
@ -1022,13 +1018,13 @@ category = "Assets"
|
|||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "custom_asset_io"
|
||||
path = "examples/asset/custom_asset_io.rs"
|
||||
name = "custom_asset_reader"
|
||||
path = "examples/asset/custom_asset_reader.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.custom_asset_io]
|
||||
[package.metadata.example.custom_asset_reader]
|
||||
name = "Custom Asset IO"
|
||||
description = "Implements a custom asset io loader"
|
||||
description = "Implements a custom AssetReader"
|
||||
category = "Assets"
|
||||
wasm = true
|
||||
|
||||
|
@ -1043,6 +1039,18 @@ description = "Demonstrates automatic reloading of assets when modified on disk"
|
|||
category = "Assets"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "asset_processing"
|
||||
path = "examples/asset/processing/processing.rs"
|
||||
doc-scrape-examples = true
|
||||
required-features = ["filesystem_watcher"]
|
||||
|
||||
[package.metadata.example.asset_processing]
|
||||
name = "Asset Processing"
|
||||
description = "Demonstrates how to process and load custom assets"
|
||||
category = "Assets"
|
||||
wasm = false
|
||||
|
||||
# Async Tasks
|
||||
[[example]]
|
||||
name = "async_compute"
|
||||
|
|
|
@ -7,12 +7,12 @@ use std::ops::Deref;
|
|||
use std::time::Duration;
|
||||
|
||||
use bevy_app::{App, Plugin, PostUpdate};
|
||||
use bevy_asset::{AddAsset, Assets, Handle};
|
||||
use bevy_asset::{Asset, AssetApp, Assets, Handle};
|
||||
use bevy_core::Name;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_hierarchy::{Children, Parent};
|
||||
use bevy_math::{Quat, Vec3};
|
||||
use bevy_reflect::{Reflect, TypeUuid};
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_render::mesh::morph::MorphWeights;
|
||||
use bevy_time::Time;
|
||||
use bevy_transform::{prelude::Transform, TransformSystem};
|
||||
|
@ -65,8 +65,7 @@ pub struct EntityPath {
|
|||
}
|
||||
|
||||
/// A list of [`VariableCurve`], and the [`EntityPath`] to which they apply.
|
||||
#[derive(Reflect, Clone, TypeUuid, Debug, Default)]
|
||||
#[uuid = "d81b7179-0448-4eb0-89fe-c067222725bf"]
|
||||
#[derive(Asset, Reflect, Clone, Debug, Default)]
|
||||
pub struct AnimationClip {
|
||||
curves: Vec<Vec<VariableCurve>>,
|
||||
paths: HashMap<EntityPath, usize>,
|
||||
|
@ -734,7 +733,7 @@ pub struct AnimationPlugin;
|
|||
|
||||
impl Plugin for AnimationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_asset::<AnimationClip>()
|
||||
app.init_asset::<AnimationClip>()
|
||||
.register_asset_reflect::<AnimationClip>()
|
||||
.register_type::<AnimationPlayer>()
|
||||
.add_systems(
|
||||
|
|
|
@ -8,31 +8,35 @@ repository = "https://github.com/bevyengine/bevy"
|
|||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["bevy"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
default = []
|
||||
filesystem_watcher = ["notify"]
|
||||
debug_asset_server = ["filesystem_watcher"]
|
||||
filesystem_watcher = ["notify-debouncer-full"]
|
||||
multi-threaded = ["bevy_tasks/multi-threaded"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../bevy_app", version = "0.12.0-dev" }
|
||||
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.12.0-dev" }
|
||||
bevy_asset_macros = { path = "macros", version = "0.12.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.12.0-dev" }
|
||||
bevy_log = { path = "../bevy_log", version = "0.12.0-dev" }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.12.0-dev", features = ["bevy"] }
|
||||
bevy_reflect = { path = "../bevy_reflect", version = "0.12.0-dev" }
|
||||
bevy_tasks = { path = "../bevy_tasks", version = "0.12.0-dev" }
|
||||
bevy_utils = { path = "../bevy_utils", version = "0.12.0-dev" }
|
||||
|
||||
# other
|
||||
anyhow = "1.0"
|
||||
async-broadcast = "0.5"
|
||||
async-fs = "1.5"
|
||||
async-lock = "2.8"
|
||||
crossbeam-channel = "0.5"
|
||||
downcast-rs = "1.2"
|
||||
futures-io = "0.3"
|
||||
futures-lite = "1.12"
|
||||
md5 = "0.7"
|
||||
parking_lot = { version = "0.12", features = ["arc_lock", "send_guard"] }
|
||||
ron = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
crossbeam-channel = "0.5.0"
|
||||
anyhow = "1.0.4"
|
||||
thiserror = "1.0"
|
||||
downcast-rs = "1.2.0"
|
||||
fastrand = "1.7.0"
|
||||
notify = { version = "6.0.0", optional = true }
|
||||
parking_lot = "0.12.1"
|
||||
async-channel = "1.4.2"
|
||||
notify-debouncer-full = { version = "0.2.0", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
bevy_winit = { path = "../bevy_winit", version = "0.12.0-dev" }
|
||||
|
@ -44,6 +48,4 @@ wasm-bindgen-futures = "0.4"
|
|||
js-sys = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
futures-lite = "1.4.0"
|
||||
tempfile = "3.2.0"
|
||||
bevy_core = { path = "../bevy_core", version = "0.12.0-dev" }
|
||||
bevy_core = { path = "../bevy_core", version = "0.12.0-dev" }
|
19
crates/bevy_asset/macros/Cargo.toml
Normal file
19
crates/bevy_asset/macros/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "bevy_asset_macros"
|
||||
version = "0.12.0-dev"
|
||||
edition = "2021"
|
||||
description = "Derive implementations for bevy_asset"
|
||||
homepage = "https://bevyengine.org"
|
||||
repository = "https://github.com/bevyengine/bevy"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["bevy"]
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.12.0-dev" }
|
||||
|
||||
syn = "2.0"
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
76
crates/bevy_asset/macros/src/lib.rs
Normal file
76
crates/bevy_asset/macros/src/lib.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use bevy_macro_utils::BevyManifest;
|
||||
use proc_macro::{Span, TokenStream};
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, Data, DeriveInput, Path};
|
||||
|
||||
pub(crate) fn bevy_asset_path() -> syn::Path {
|
||||
BevyManifest::default().get_path("bevy_asset")
|
||||
}
|
||||
|
||||
const DEPENDENCY_ATTRIBUTE: &str = "dependency";
|
||||
|
||||
#[proc_macro_derive(Asset, attributes(dependency))]
|
||||
pub fn derive_asset(input: TokenStream) -> TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
let bevy_asset_path: Path = bevy_asset_path();
|
||||
|
||||
let struct_name = &ast.ident;
|
||||
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
|
||||
let dependency_visitor = match derive_dependency_visitor_internal(&ast, &bevy_asset_path) {
|
||||
Ok(dependency_visitor) => dependency_visitor,
|
||||
Err(err) => return err.into_compile_error().into(),
|
||||
};
|
||||
|
||||
TokenStream::from(quote! {
|
||||
impl #impl_generics #bevy_asset_path::Asset for #struct_name #type_generics #where_clause { }
|
||||
#dependency_visitor
|
||||
})
|
||||
}
|
||||
|
||||
#[proc_macro_derive(VisitAssetDependencies, attributes(dependency))]
|
||||
pub fn derive_asset_dependency_visitor(input: TokenStream) -> TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
let bevy_asset_path: Path = bevy_asset_path();
|
||||
match derive_dependency_visitor_internal(&ast, &bevy_asset_path) {
|
||||
Ok(dependency_visitor) => TokenStream::from(dependency_visitor),
|
||||
Err(err) => err.into_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn derive_dependency_visitor_internal(
|
||||
ast: &DeriveInput,
|
||||
bevy_asset_path: &Path,
|
||||
) -> Result<proc_macro2::TokenStream, syn::Error> {
|
||||
let mut field_visitors = Vec::new();
|
||||
if let Data::Struct(data_struct) = &ast.data {
|
||||
for field in data_struct.fields.iter() {
|
||||
if field
|
||||
.attrs
|
||||
.iter()
|
||||
.any(|a| a.path().is_ident(DEPENDENCY_ATTRIBUTE))
|
||||
{
|
||||
if let Some(field_ident) = &field.ident {
|
||||
field_visitors.push(quote! {
|
||||
#bevy_asset_path::VisitAssetDependencies::visit_dependencies(&self.#field_ident, visit);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
Span::call_site().into(),
|
||||
"Asset derive currently only works on structs",
|
||||
));
|
||||
}
|
||||
|
||||
let struct_name = &ast.ident;
|
||||
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
|
||||
|
||||
Ok(quote! {
|
||||
impl #impl_generics #bevy_asset_path::VisitAssetDependencies for #struct_name #type_generics #where_clause {
|
||||
fn visit_dependencies(&self, visit: &mut impl FnMut(#bevy_asset_path::UntypedAssetId)) {
|
||||
#(#field_visitors)*
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,143 +0,0 @@
|
|||
//! Support for hot reloading internal assets.
|
||||
//!
|
||||
//! Internal assets (e.g. shaders) are bundled directly into an application and can't be hot
|
||||
//! reloaded using the conventional API.
|
||||
use bevy_app::{App, Plugin, Update};
|
||||
use bevy_ecs::{prelude::*, system::SystemState};
|
||||
use bevy_tasks::{IoTaskPool, TaskPoolBuilder};
|
||||
use bevy_utils::{Duration, HashMap};
|
||||
use std::{
|
||||
ops::{Deref, DerefMut},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Asset, AssetEvent, AssetPlugin, AssetServer, Assets, ChangeWatcher, FileAssetIo, Handle,
|
||||
HandleUntyped,
|
||||
};
|
||||
|
||||
/// A helper [`App`] used for hot reloading internal assets, which are compiled-in to Bevy plugins.
|
||||
pub struct DebugAssetApp(App);
|
||||
|
||||
impl Deref for DebugAssetApp {
|
||||
type Target = App;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for DebugAssetApp {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// A set containing the system that runs [`DebugAssetApp`].
|
||||
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct DebugAssetAppRun;
|
||||
|
||||
/// Facilitates the creation of a "debug asset app", whose sole responsibility is hot reloading
|
||||
/// assets that are "internal" / compiled-in to Bevy Plugins.
|
||||
///
|
||||
/// Pair with the [`load_internal_asset`](crate::load_internal_asset) macro to load hot-reloadable
|
||||
/// assets. The `debug_asset_server` feature flag must also be enabled for hot reloading to work.
|
||||
/// Currently only hot reloads assets stored in the `crates` folder.
|
||||
#[derive(Default)]
|
||||
pub struct DebugAssetServerPlugin;
|
||||
|
||||
/// A collection that maps internal assets in a [`DebugAssetApp`]'s asset server to their mirrors in
|
||||
/// the main [`App`].
|
||||
#[derive(Resource)]
|
||||
pub struct HandleMap<T: Asset> {
|
||||
/// The collection of asset handles.
|
||||
pub handles: HashMap<Handle<T>, Handle<T>>,
|
||||
}
|
||||
|
||||
impl<T: Asset> Default for HandleMap<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
handles: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for DebugAssetServerPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
IoTaskPool::init(|| {
|
||||
TaskPoolBuilder::default()
|
||||
.num_threads(2)
|
||||
.thread_name("Debug Asset Server IO Task Pool".to_string())
|
||||
.build()
|
||||
});
|
||||
let mut debug_asset_app = App::new();
|
||||
debug_asset_app.add_plugins(AssetPlugin {
|
||||
asset_folder: "crates".to_string(),
|
||||
watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200)),
|
||||
});
|
||||
app.insert_non_send_resource(DebugAssetApp(debug_asset_app));
|
||||
app.add_systems(Update, run_debug_asset_app);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_debug_asset_app(mut debug_asset_app: NonSendMut<DebugAssetApp>) {
|
||||
debug_asset_app.0.update();
|
||||
}
|
||||
|
||||
pub(crate) fn sync_debug_assets<T: Asset + Clone>(
|
||||
mut debug_asset_app: NonSendMut<DebugAssetApp>,
|
||||
mut assets: ResMut<Assets<T>>,
|
||||
) {
|
||||
let world = &mut debug_asset_app.0.world;
|
||||
let mut state = SystemState::<(
|
||||
Res<Events<AssetEvent<T>>>,
|
||||
Res<HandleMap<T>>,
|
||||
Res<Assets<T>>,
|
||||
)>::new(world);
|
||||
let (changed_shaders, handle_map, debug_assets) = state.get_mut(world);
|
||||
for changed in changed_shaders.iter_current_update_events() {
|
||||
let debug_handle = match changed {
|
||||
AssetEvent::Created { handle } | AssetEvent::Modified { handle } => handle,
|
||||
AssetEvent::Removed { .. } => continue,
|
||||
};
|
||||
if let Some(handle) = handle_map.handles.get(debug_handle) {
|
||||
if let Some(debug_asset) = debug_assets.get(debug_handle) {
|
||||
assets.set_untracked(handle, debug_asset.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses the return type of the given loader to register the given handle with the appropriate type
|
||||
/// and load the asset with the given `path` and parent `file_path`.
|
||||
///
|
||||
/// If this feels a bit odd ... that's because it is. This was built to improve the UX of the
|
||||
/// `load_internal_asset` macro.
|
||||
pub fn register_handle_with_loader<A: Asset, T>(
|
||||
_loader: fn(T, String) -> A,
|
||||
app: &mut DebugAssetApp,
|
||||
handle: HandleUntyped,
|
||||
file_path: &str,
|
||||
path: &'static str,
|
||||
) {
|
||||
let mut state = SystemState::<(ResMut<HandleMap<A>>, Res<AssetServer>)>::new(&mut app.world);
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let manifest_dir_path = Path::new(&manifest_dir);
|
||||
let (mut handle_map, asset_server) = state.get_mut(&mut app.world);
|
||||
let asset_io = asset_server
|
||||
.asset_io()
|
||||
.downcast_ref::<FileAssetIo>()
|
||||
.expect("The debug AssetServer only works with FileAssetIo-backed AssetServers");
|
||||
let absolute_file_path = manifest_dir_path.join(
|
||||
Path::new(file_path)
|
||||
.parent()
|
||||
.expect("file path must have a parent"),
|
||||
);
|
||||
let asset_folder_relative_path = absolute_file_path
|
||||
.strip_prefix(asset_io.root_path())
|
||||
.expect("The AssetIo root path should be a prefix of the absolute file path");
|
||||
handle_map.handles.insert(
|
||||
asset_server.load(asset_folder_relative_path.join(path)),
|
||||
handle.clone_weak().typed::<A>(),
|
||||
);
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
use crate::{Asset, Assets};
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_diagnostic::{
|
||||
Diagnostic, DiagnosticId, Diagnostics, DiagnosticsStore, MAX_DIAGNOSTIC_NAME_WIDTH,
|
||||
};
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
/// Adds an asset count diagnostic to an [`App`] for assets of type `T`.
|
||||
pub struct AssetCountDiagnosticsPlugin<T: Asset> {
|
||||
marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: Asset> Default for AssetCountDiagnosticsPlugin<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
marker: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> Plugin for AssetCountDiagnosticsPlugin<T> {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, Self::setup_system)
|
||||
.add_systems(Update, Self::diagnostic_system);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> AssetCountDiagnosticsPlugin<T> {
|
||||
/// Gets unique id of this diagnostic.
|
||||
///
|
||||
/// The diagnostic id is the type uuid of `T`.
|
||||
pub fn diagnostic_id() -> DiagnosticId {
|
||||
DiagnosticId(T::TYPE_UUID)
|
||||
}
|
||||
|
||||
/// Registers the asset count diagnostic for the current application.
|
||||
pub fn setup_system(mut diagnostics: ResMut<DiagnosticsStore>) {
|
||||
let asset_type_name = std::any::type_name::<T>();
|
||||
let max_length = MAX_DIAGNOSTIC_NAME_WIDTH - "asset_count ".len();
|
||||
diagnostics.add(Diagnostic::new(
|
||||
Self::diagnostic_id(),
|
||||
format!(
|
||||
"asset_count {}",
|
||||
if asset_type_name.len() > max_length {
|
||||
asset_type_name
|
||||
.split_at(asset_type_name.len() - max_length + 1)
|
||||
.1
|
||||
} else {
|
||||
asset_type_name
|
||||
}
|
||||
),
|
||||
20,
|
||||
));
|
||||
}
|
||||
|
||||
/// Updates the asset count of `T` assets.
|
||||
pub fn diagnostic_system(mut diagnostics: Diagnostics, assets: Res<Assets<T>>) {
|
||||
diagnostics.add_measurement(Self::diagnostic_id(), || assets.len() as f64);
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
//! Diagnostic providers for `bevy_diagnostic`.
|
||||
|
||||
mod asset_count_diagnostics_plugin;
|
||||
pub use asset_count_diagnostics_plugin::AssetCountDiagnosticsPlugin;
|
77
crates/bevy_asset/src/event.rs
Normal file
77
crates/bevy_asset/src/event.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use crate::{Asset, AssetId};
|
||||
use bevy_ecs::event::Event;
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Events that occur for a specific [`Asset`], such as "value changed" events and "dependency" events.
|
||||
#[derive(Event)]
|
||||
pub enum AssetEvent<A: Asset> {
|
||||
/// Emitted whenever an [`Asset`] is added.
|
||||
Added { id: AssetId<A> },
|
||||
/// Emitted whenever an [`Asset`] value is modified.
|
||||
Modified { id: AssetId<A> },
|
||||
/// Emitted whenever an [`Asset`] is removed.
|
||||
Removed { id: AssetId<A> },
|
||||
/// Emitted whenever an [`Asset`] has been fully loaded (including its dependencies and all "recursive dependencies").
|
||||
LoadedWithDependencies { id: AssetId<A> },
|
||||
}
|
||||
|
||||
impl<A: Asset> AssetEvent<A> {
|
||||
/// Returns `true` if this event is [`AssetEvent::LoadedWithDependencies`] and matches the given `id`.
|
||||
pub fn is_loaded_with_dependencies(&self, asset_id: impl Into<AssetId<A>>) -> bool {
|
||||
matches!(self, AssetEvent::LoadedWithDependencies { id } if *id == asset_id.into())
|
||||
}
|
||||
|
||||
/// Returns `true` if this event is [`AssetEvent::Added`] and matches the given `id`.
|
||||
pub fn is_added(&self, asset_id: impl Into<AssetId<A>>) -> bool {
|
||||
matches!(self, AssetEvent::Added { id } if *id == asset_id.into())
|
||||
}
|
||||
|
||||
/// Returns `true` if this event is [`AssetEvent::Modified`] and matches the given `id`.
|
||||
pub fn is_modified(&self, asset_id: impl Into<AssetId<A>>) -> bool {
|
||||
matches!(self, AssetEvent::Modified { id } if *id == asset_id.into())
|
||||
}
|
||||
|
||||
/// Returns `true` if this event is [`AssetEvent::Removed`] and matches the given `id`.
|
||||
pub fn is_removed(&self, asset_id: impl Into<AssetId<A>>) -> bool {
|
||||
matches!(self, AssetEvent::Removed { id } if *id == asset_id.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> Clone for AssetEvent<A> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> Copy for AssetEvent<A> {}
|
||||
|
||||
impl<A: Asset> Debug for AssetEvent<A> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Added { id } => f.debug_struct("Added").field("id", id).finish(),
|
||||
Self::Modified { id } => f.debug_struct("Modified").field("id", id).finish(),
|
||||
Self::Removed { id } => f.debug_struct("Removed").field("id", id).finish(),
|
||||
Self::LoadedWithDependencies { id } => f
|
||||
.debug_struct("LoadedWithDependencies")
|
||||
.field("id", id)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> PartialEq for AssetEvent<A> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Added { id: l_id }, Self::Added { id: r_id })
|
||||
| (Self::Modified { id: l_id }, Self::Modified { id: r_id })
|
||||
| (Self::Removed { id: l_id }, Self::Removed { id: r_id })
|
||||
| (
|
||||
Self::LoadedWithDependencies { id: l_id },
|
||||
Self::LoadedWithDependencies { id: r_id },
|
||||
) => l_id == r_id,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> Eq for AssetEvent<A> {}
|
|
@ -1,46 +0,0 @@
|
|||
use bevy_utils::{default, Duration, HashMap, HashSet};
|
||||
use crossbeam_channel::Receiver;
|
||||
use notify::{Event, RecommendedWatcher, RecursiveMode, Result, Watcher};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::ChangeWatcher;
|
||||
|
||||
/// Watches for changes to files on the local filesystem.
|
||||
///
|
||||
/// When hot-reloading is enabled, the [`AssetServer`](crate::AssetServer) uses this to reload
|
||||
/// assets when their source files are modified.
|
||||
pub struct FilesystemWatcher {
|
||||
pub watcher: RecommendedWatcher,
|
||||
pub receiver: Receiver<Result<Event>>,
|
||||
pub path_map: HashMap<PathBuf, HashSet<PathBuf>>,
|
||||
pub delay: Duration,
|
||||
}
|
||||
|
||||
impl FilesystemWatcher {
|
||||
pub fn new(configuration: &ChangeWatcher) -> Self {
|
||||
let (sender, receiver) = crossbeam_channel::unbounded();
|
||||
let watcher: RecommendedWatcher = RecommendedWatcher::new(
|
||||
move |res| {
|
||||
sender.send(res).expect("Watch event send failure.");
|
||||
},
|
||||
default(),
|
||||
)
|
||||
.expect("Failed to create filesystem watcher.");
|
||||
FilesystemWatcher {
|
||||
watcher,
|
||||
receiver,
|
||||
path_map: default(),
|
||||
delay: configuration.delay,
|
||||
}
|
||||
}
|
||||
|
||||
/// Watch for changes recursively at the provided path.
|
||||
pub fn watch<P: AsRef<Path>>(&mut self, to_watch: P, to_reload: PathBuf) -> Result<()> {
|
||||
self.path_map
|
||||
.entry(to_watch.as_ref().to_owned())
|
||||
.or_default()
|
||||
.insert(to_reload);
|
||||
self.watcher
|
||||
.watch(to_watch.as_ref(), RecursiveMode::Recursive)
|
||||
}
|
||||
}
|
12
crates/bevy_asset/src/folder.rs
Normal file
12
crates/bevy_asset/src/folder.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use crate as bevy_asset;
|
||||
use crate::{Asset, UntypedHandle};
|
||||
use bevy_reflect::TypePath;
|
||||
|
||||
/// A "loaded folder" containing handles for all assets stored in a given [`AssetPath`].
|
||||
///
|
||||
/// [`AssetPath`]: crate::AssetPath
|
||||
#[derive(Asset, TypePath)]
|
||||
pub struct LoadedFolder {
|
||||
#[dependency]
|
||||
pub handles: Vec<UntypedHandle>,
|
||||
}
|
|
@ -1,472 +1,387 @@
|
|||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt::Debug,
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
path::{AssetPath, AssetPathId},
|
||||
Asset, Assets,
|
||||
meta::MetaTransform, Asset, AssetId, AssetIndexAllocator, AssetPath, InternalAssetId,
|
||||
UntypedAssetId,
|
||||
};
|
||||
use bevy_ecs::{component::Component, reflect::ReflectComponent};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize};
|
||||
use bevy_utils::Uuid;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_reflect::{Reflect, TypePath, Uuid};
|
||||
use bevy_utils::get_short_name;
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
hash::{Hash, Hasher},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// A unique, stable asset id.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize, Reflect,
|
||||
)]
|
||||
#[reflect_value(Serialize, Deserialize, PartialEq, Hash)]
|
||||
pub enum HandleId {
|
||||
/// A handle id of a loaded asset.
|
||||
Id(Uuid, u64),
|
||||
|
||||
/// A handle id of a pending asset.
|
||||
AssetPathId(AssetPathId),
|
||||
/// Provides [`Handle`] and [`UntypedHandle`] _for a specific asset type_.
|
||||
/// This should _only_ be used for one specific asset type.
|
||||
#[derive(Clone)]
|
||||
pub struct AssetHandleProvider {
|
||||
pub(crate) allocator: Arc<AssetIndexAllocator>,
|
||||
pub(crate) drop_sender: Sender<DropEvent>,
|
||||
pub(crate) drop_receiver: Receiver<DropEvent>,
|
||||
pub(crate) type_id: TypeId,
|
||||
}
|
||||
|
||||
impl From<AssetPathId> for HandleId {
|
||||
fn from(value: AssetPathId) -> Self {
|
||||
HandleId::AssetPathId(value)
|
||||
}
|
||||
pub(crate) struct DropEvent {
|
||||
pub(crate) id: InternalAssetId,
|
||||
pub(crate) asset_server_managed: bool,
|
||||
}
|
||||
|
||||
impl<'a> From<AssetPath<'a>> for HandleId {
|
||||
fn from(value: AssetPath<'a>) -> Self {
|
||||
HandleId::AssetPathId(AssetPathId::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> From<&'a AssetPath<'b>> for HandleId {
|
||||
fn from(value: &'a AssetPath<'b>) -> Self {
|
||||
HandleId::AssetPathId(AssetPathId::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl HandleId {
|
||||
/// Creates a random id for an asset of type `T`.
|
||||
#[inline]
|
||||
pub fn random<T: Asset>() -> Self {
|
||||
HandleId::Id(T::TYPE_UUID, fastrand::u64(..))
|
||||
}
|
||||
|
||||
/// Creates the default id for an asset of type `T`.
|
||||
#[inline]
|
||||
#[allow(clippy::should_implement_trait)] // `Default` is not implemented for `HandleId`, the default value depends on the asset type
|
||||
pub fn default<T: Asset>() -> Self {
|
||||
HandleId::Id(T::TYPE_UUID, 0)
|
||||
}
|
||||
|
||||
/// Creates an arbitrary asset id without an explicit type bound.
|
||||
#[inline]
|
||||
pub const fn new(type_uuid: Uuid, id: u64) -> Self {
|
||||
HandleId::Id(type_uuid, id)
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle into a specific [`Asset`] of type `T`.
|
||||
///
|
||||
/// Handles contain a unique id that corresponds to a specific asset in the [`Assets`] collection.
|
||||
///
|
||||
/// # Accessing the Asset
|
||||
///
|
||||
/// A handle is _not_ the asset itself, but should be seen as a pointer to the asset. Modifying a
|
||||
/// handle's `id` only modifies which asset is being pointed to. To get the actual asset, try using
|
||||
/// [`Assets::get`] or [`Assets::get_mut`].
|
||||
///
|
||||
/// # Strong and Weak
|
||||
///
|
||||
/// A handle can be either "Strong" or "Weak". Simply put: Strong handles keep the asset loaded,
|
||||
/// while Weak handles do not affect the loaded status of assets. This is due to a type of
|
||||
/// _reference counting_. When the number of Strong handles that exist for any given asset reach
|
||||
/// zero, the asset is dropped and becomes unloaded. In some cases, you might want a reference to an
|
||||
/// asset but don't want to take the responsibility of keeping it loaded that comes with a Strong handle.
|
||||
/// This is where a Weak handle can be very useful.
|
||||
///
|
||||
/// For example, imagine you have a `Sprite` component and a `Collider` component. The `Collider` uses
|
||||
/// the `Sprite`'s image size to check for collisions. It does so by keeping a Weak copy of the
|
||||
/// `Sprite`'s Strong handle to the image asset.
|
||||
///
|
||||
/// If the `Sprite` is removed, its Strong handle to the image is dropped with it. And since it was the
|
||||
/// only Strong handle for that asset, the asset is unloaded. Our `Collider` component still has a Weak
|
||||
/// handle to the unloaded asset, but it will not be able to retrieve the image data, resulting in
|
||||
/// collisions no longer being detected for that entity.
|
||||
///
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component, Default)]
|
||||
pub struct Handle<T>
|
||||
where
|
||||
T: Asset,
|
||||
{
|
||||
id: HandleId,
|
||||
#[reflect(ignore)]
|
||||
handle_type: HandleType,
|
||||
#[reflect(ignore)]
|
||||
// NOTE: PhantomData<fn() -> T> gives this safe Send/Sync impls
|
||||
marker: PhantomData<fn() -> T>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum HandleType {
|
||||
#[default]
|
||||
Weak,
|
||||
Strong(Sender<RefChange>),
|
||||
}
|
||||
|
||||
impl Debug for HandleType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
HandleType::Weak => f.write_str("Weak"),
|
||||
HandleType::Strong(_) => f.write_str("Strong"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> Handle<T> {
|
||||
pub(crate) fn strong(id: HandleId, ref_change_sender: Sender<RefChange>) -> Self {
|
||||
ref_change_sender.send(RefChange::Increment(id)).unwrap();
|
||||
impl AssetHandleProvider {
|
||||
pub(crate) fn new(type_id: TypeId, allocator: Arc<AssetIndexAllocator>) -> Self {
|
||||
let (drop_sender, drop_receiver) = crossbeam_channel::unbounded();
|
||||
Self {
|
||||
id,
|
||||
handle_type: HandleType::Strong(ref_change_sender),
|
||||
marker: PhantomData,
|
||||
type_id,
|
||||
allocator,
|
||||
drop_sender,
|
||||
drop_receiver,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a weak handle into an Asset identified by `id`.
|
||||
#[inline]
|
||||
pub fn weak(id: HandleId) -> Self {
|
||||
Self {
|
||||
id,
|
||||
handle_type: HandleType::Weak,
|
||||
marker: PhantomData,
|
||||
}
|
||||
/// Reserves a new strong [`UntypedHandle`] (with a new [`UntypedAssetId`]). The stored [`Asset`] [`TypeId`] in the
|
||||
/// [`UntypedHandle`] will match the [`Asset`] [`TypeId`] assigned to this [`AssetHandleProvider`].
|
||||
pub fn reserve_handle(&self) -> UntypedHandle {
|
||||
let index = self.allocator.reserve();
|
||||
UntypedHandle::Strong(self.get_handle(InternalAssetId::Index(index), false, None, None))
|
||||
}
|
||||
|
||||
/// The ID of the asset as contained within its respective [`Assets`] collection.
|
||||
#[inline]
|
||||
pub fn id(&self) -> HandleId {
|
||||
self.id
|
||||
pub(crate) fn get_handle(
|
||||
&self,
|
||||
id: InternalAssetId,
|
||||
asset_server_managed: bool,
|
||||
path: Option<AssetPath<'static>>,
|
||||
meta_transform: Option<MetaTransform>,
|
||||
) -> Arc<StrongHandle> {
|
||||
Arc::new(StrongHandle {
|
||||
id: id.untyped(self.type_id),
|
||||
drop_sender: self.drop_sender.clone(),
|
||||
meta_transform,
|
||||
path,
|
||||
asset_server_managed,
|
||||
})
|
||||
}
|
||||
|
||||
/// Recasts this handle as a weak handle of an Asset `U`.
|
||||
pub fn cast_weak<U: Asset>(&self) -> Handle<U> {
|
||||
let id = if let HandleId::Id(_, id) = self.id {
|
||||
HandleId::Id(U::TYPE_UUID, id)
|
||||
} else {
|
||||
self.id
|
||||
};
|
||||
|
||||
Handle {
|
||||
id,
|
||||
handle_type: HandleType::Weak,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a weak handle.
|
||||
pub fn is_weak(&self) -> bool {
|
||||
matches!(self.handle_type, HandleType::Weak)
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a strong handle.
|
||||
pub fn is_strong(&self) -> bool {
|
||||
matches!(self.handle_type, HandleType::Strong(_))
|
||||
}
|
||||
|
||||
/// Makes this handle Strong if it wasn't already.
|
||||
///
|
||||
/// This method requires the corresponding [`Assets`](crate::Assets) collection.
|
||||
pub fn make_strong(&mut self, assets: &Assets<T>) {
|
||||
if self.is_strong() {
|
||||
return;
|
||||
}
|
||||
let sender = assets.ref_change_sender.clone();
|
||||
sender.send(RefChange::Increment(self.id)).unwrap();
|
||||
self.handle_type = HandleType::Strong(sender);
|
||||
}
|
||||
|
||||
/// Creates a weak copy of this handle.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn clone_weak(&self) -> Self {
|
||||
Self::weak(self.id)
|
||||
}
|
||||
|
||||
/// Creates an untyped copy of this handle.
|
||||
pub fn clone_untyped(&self) -> HandleUntyped {
|
||||
match &self.handle_type {
|
||||
HandleType::Strong(sender) => HandleUntyped::strong(self.id, sender.clone()),
|
||||
HandleType::Weak => HandleUntyped::weak(self.id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a weak, untyped copy of this handle.
|
||||
pub fn clone_weak_untyped(&self) -> HandleUntyped {
|
||||
HandleUntyped::weak(self.id)
|
||||
pub(crate) fn reserve_handle_internal(
|
||||
&self,
|
||||
asset_server_managed: bool,
|
||||
path: Option<AssetPath<'static>>,
|
||||
meta_transform: Option<MetaTransform>,
|
||||
) -> Arc<StrongHandle> {
|
||||
let index = self.allocator.reserve();
|
||||
self.get_handle(
|
||||
InternalAssetId::Index(index),
|
||||
asset_server_managed,
|
||||
path,
|
||||
meta_transform,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> Drop for Handle<T> {
|
||||
/// The internal "strong" [`Asset`] handle storage for [`Handle::Strong`] and [`UntypedHandle::Strong`]. When this is dropped,
|
||||
/// the [`Asset`] will be freed. It also stores some asset metadata for easy access from handles.
|
||||
#[derive(TypePath)]
|
||||
pub struct StrongHandle {
|
||||
pub(crate) id: UntypedAssetId,
|
||||
pub(crate) asset_server_managed: bool,
|
||||
pub(crate) path: Option<AssetPath<'static>>,
|
||||
/// Modifies asset meta. This is stored on the handle because it is:
|
||||
/// 1. configuration tied to the lifetime of a specific asset load
|
||||
/// 2. configuration that must be repeatable when the asset is hot-reloaded
|
||||
pub(crate) meta_transform: Option<MetaTransform>,
|
||||
pub(crate) drop_sender: Sender<DropEvent>,
|
||||
}
|
||||
|
||||
impl Drop for StrongHandle {
|
||||
fn drop(&mut self) {
|
||||
match self.handle_type {
|
||||
HandleType::Strong(ref sender) => {
|
||||
// ignore send errors because this means the channel is shut down / the game has
|
||||
// stopped
|
||||
let _ = sender.send(RefChange::Decrement(self.id));
|
||||
}
|
||||
HandleType::Weak => {}
|
||||
if let Err(err) = self.drop_sender.send(DropEvent {
|
||||
id: self.id.internal(),
|
||||
asset_server_managed: self.asset_server_managed,
|
||||
}) {
|
||||
println!("Failed to send DropEvent for StrongHandle {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> From<Handle<T>> for HandleId {
|
||||
fn from(value: Handle<T>) -> Self {
|
||||
value.id
|
||||
impl std::fmt::Debug for StrongHandle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("StrongHandle")
|
||||
.field("id", &self.id)
|
||||
.field("asset_server_managed", &self.asset_server_managed)
|
||||
.field("path", &self.path)
|
||||
.field("drop_sender", &self.drop_sender)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HandleUntyped> for HandleId {
|
||||
fn from(value: HandleUntyped) -> Self {
|
||||
value.id
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for HandleId {
|
||||
fn from(value: &str) -> Self {
|
||||
AssetPathId::from(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&String> for HandleId {
|
||||
fn from(value: &String) -> Self {
|
||||
AssetPathId::from(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for HandleId {
|
||||
fn from(value: String) -> Self {
|
||||
AssetPathId::from(&value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> From<&Handle<T>> for HandleId {
|
||||
fn from(value: &Handle<T>) -> Self {
|
||||
value.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> Hash for Handle<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
Hash::hash(&self.id, state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> PartialEq for Handle<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> Eq for Handle<T> {}
|
||||
|
||||
impl<T: Asset> PartialOrd for Handle<T> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> Ord for Handle<T> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.id.cmp(&other.id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> Default for Handle<T> {
|
||||
fn default() -> Self {
|
||||
Handle::weak(HandleId::default::<T>())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Asset> Debug for Handle<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
||||
let name = std::any::type_name::<T>().split("::").last().unwrap();
|
||||
write!(f, "{:?}Handle<{name}>({:?})", self.handle_type, self.id)
|
||||
}
|
||||
/// A strong or weak handle to a specific [`Asset`]. If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept
|
||||
/// alive until the [`Handle`] is dropped. If a [`Handle`] is [`Handle::Weak`], it does not necessarily reference a live [`Asset`],
|
||||
/// nor will it keep assets alive.
|
||||
///
|
||||
/// [`Handle`] can be cloned. If a [`Handle::Strong`] is cloned, the referenced [`Asset`] will not be freed until _all_ instances
|
||||
/// of the [`Handle`] are dropped.
|
||||
///
|
||||
/// [`Handle::Strong`] also provides access to useful [`Asset`] metadata, such as the [`AssetPath`] (if it exists).
|
||||
#[derive(Component, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub enum Handle<A: Asset> {
|
||||
/// A "strong" reference to a live (or loading) [`Asset`]. If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept
|
||||
/// alive until the [`Handle`] is dropped. Strong handles also provide access to additional asset metadata.
|
||||
Strong(Arc<StrongHandle>),
|
||||
/// A "weak" reference to an [`Asset`]. If a [`Handle`] is [`Handle::Weak`], it does not necessarily reference a live [`Asset`],
|
||||
/// nor will it keep assets alive.
|
||||
Weak(AssetId<A>),
|
||||
}
|
||||
|
||||
impl<T: Asset> Clone for Handle<T> {
|
||||
fn clone(&self) -> Self {
|
||||
match self.handle_type {
|
||||
HandleType::Strong(ref sender) => Handle::strong(self.id, sender.clone()),
|
||||
HandleType::Weak => Handle::weak(self.id),
|
||||
match self {
|
||||
Handle::Strong(handle) => Handle::Strong(handle.clone()),
|
||||
Handle::Weak(id) => Handle::Weak(*id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A non-generic version of [`Handle`].
|
||||
///
|
||||
/// This allows handles to be mingled in a cross asset context. For example, storing `Handle<A>` and
|
||||
/// `Handle<B>` in the same `HashSet<HandleUntyped>`.
|
||||
///
|
||||
/// To convert back to a typed handle, use the [typed](HandleUntyped::typed) method.
|
||||
#[derive(Debug)]
|
||||
pub struct HandleUntyped {
|
||||
id: HandleId,
|
||||
handle_type: HandleType,
|
||||
}
|
||||
|
||||
impl HandleUntyped {
|
||||
/// Creates a weak untyped handle with an arbitrary id.
|
||||
pub const fn weak_from_u64(uuid: Uuid, id: u64) -> Self {
|
||||
Self {
|
||||
id: HandleId::new(uuid, id),
|
||||
handle_type: HandleType::Weak,
|
||||
}
|
||||
impl<A: Asset> Handle<A> {
|
||||
/// Create a new [`Handle::Weak`] with the given [`u128`] encoding of a [`Uuid`].
|
||||
pub const fn weak_from_u128(value: u128) -> Self {
|
||||
Handle::Weak(AssetId::Uuid {
|
||||
uuid: Uuid::from_u128(value),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn strong(id: HandleId, ref_change_sender: Sender<RefChange>) -> Self {
|
||||
ref_change_sender.send(RefChange::Increment(id)).unwrap();
|
||||
Self {
|
||||
id,
|
||||
handle_type: HandleType::Strong(ref_change_sender),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a weak, untyped handle into an Asset identified by `id`.
|
||||
pub fn weak(id: HandleId) -> Self {
|
||||
Self {
|
||||
id,
|
||||
handle_type: HandleType::Weak,
|
||||
}
|
||||
}
|
||||
|
||||
/// The ID of the asset.
|
||||
/// Returns the [`AssetId`] of this [`Asset`].
|
||||
#[inline]
|
||||
pub fn id(&self) -> HandleId {
|
||||
self.id
|
||||
pub fn id(&self) -> AssetId<A> {
|
||||
match self {
|
||||
Handle::Strong(handle) => handle.id.typed_unchecked(),
|
||||
Handle::Weak(id) => *id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a weak copy of this handle.
|
||||
#[must_use]
|
||||
pub fn clone_weak(&self) -> Self {
|
||||
Self::weak(self.id)
|
||||
/// Returns the path if this is (1) a strong handle and (2) the asset has a path
|
||||
#[inline]
|
||||
pub fn path(&self) -> Option<&AssetPath<'static>> {
|
||||
match self {
|
||||
Handle::Strong(handle) => handle.path.as_ref(),
|
||||
Handle::Weak(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a weak handle.
|
||||
#[inline]
|
||||
pub fn is_weak(&self) -> bool {
|
||||
matches!(self.handle_type, HandleType::Weak)
|
||||
matches!(self, Handle::Weak(_))
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a strong handle.
|
||||
#[inline]
|
||||
pub fn is_strong(&self) -> bool {
|
||||
matches!(self.handle_type, HandleType::Strong(_))
|
||||
matches!(self, Handle::Strong(_))
|
||||
}
|
||||
|
||||
/// Create a weak typed [`Handle`] from this handle.
|
||||
///
|
||||
/// If this handle is strong and dropped, there is no guarantee that the asset
|
||||
/// will still be available (if only the returned handle is kept)
|
||||
pub fn typed_weak<T: Asset>(&self) -> Handle<T> {
|
||||
self.clone_weak().typed()
|
||||
}
|
||||
|
||||
/// Converts this handle into a typed [`Handle`] of an [`Asset`] `T`.
|
||||
///
|
||||
/// The new handle will maintain the Strong or Weak status of the current handle.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Will panic if type `T` doesn't match this handle's actual asset type.
|
||||
pub fn typed<T: Asset>(mut self) -> Handle<T> {
|
||||
if let HandleId::Id(type_uuid, _) = self.id {
|
||||
assert!(
|
||||
T::TYPE_UUID == type_uuid,
|
||||
"Attempted to convert handle to invalid type."
|
||||
);
|
||||
/// Creates a [`Handle::Weak`] clone of this [`Handle`], which will not keep the referenced [`Asset`] alive.
|
||||
#[inline]
|
||||
pub fn clone_weak(&self) -> Self {
|
||||
match self {
|
||||
Handle::Strong(handle) => Handle::Weak(handle.id.typed_unchecked::<A>()),
|
||||
Handle::Weak(id) => Handle::Weak(*id),
|
||||
}
|
||||
let handle_type = match &self.handle_type {
|
||||
HandleType::Strong(sender) => HandleType::Strong(sender.clone()),
|
||||
HandleType::Weak => HandleType::Weak,
|
||||
};
|
||||
// ensure we don't send the RefChange event when "self" is dropped
|
||||
self.handle_type = HandleType::Weak;
|
||||
Handle {
|
||||
handle_type,
|
||||
id: self.id,
|
||||
marker: PhantomData,
|
||||
}
|
||||
|
||||
/// Converts this [`Handle`] to an "untyped" / "generic-less" [`UntypedHandle`], which stores the [`Asset`] type information
|
||||
/// _inside_ [`UntypedHandle`]. This will return [`UntypedHandle::Strong`] for [`Handle::Strong`] and [`UntypedHandle::Weak`] for
|
||||
/// [`Handle::Weak`].
|
||||
#[inline]
|
||||
pub fn untyped(self) -> UntypedHandle {
|
||||
match self {
|
||||
Handle::Strong(handle) => UntypedHandle::Strong(handle),
|
||||
Handle::Weak(id) => UntypedHandle::Weak(id.untyped()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HandleUntyped {
|
||||
fn drop(&mut self) {
|
||||
match self.handle_type {
|
||||
HandleType::Strong(ref sender) => {
|
||||
// ignore send errors because this means the channel is shut down / the game has
|
||||
// stopped
|
||||
let _ = sender.send(RefChange::Decrement(self.id));
|
||||
}
|
||||
HandleType::Weak => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> From<Handle<A>> for HandleUntyped {
|
||||
fn from(mut handle: Handle<A>) -> Self {
|
||||
let handle_type = std::mem::replace(&mut handle.handle_type, HandleType::Weak);
|
||||
HandleUntyped {
|
||||
id: handle.id,
|
||||
handle_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&HandleUntyped> for HandleId {
|
||||
fn from(value: &HandleUntyped) -> Self {
|
||||
value.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for HandleUntyped {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
Hash::hash(&self.id, state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for HandleUntyped {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for HandleUntyped {}
|
||||
|
||||
impl Clone for HandleUntyped {
|
||||
fn clone(&self) -> Self {
|
||||
match self.handle_type {
|
||||
HandleType::Strong(ref sender) => HandleUntyped::strong(self.id, sender.clone()),
|
||||
HandleType::Weak => HandleUntyped::weak(self.id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum RefChange {
|
||||
Increment(HandleId),
|
||||
Decrement(HandleId),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RefChangeChannel {
|
||||
pub sender: Sender<RefChange>,
|
||||
pub receiver: Receiver<RefChange>,
|
||||
}
|
||||
|
||||
impl Default for RefChangeChannel {
|
||||
impl<A: Asset> Default for Handle<A> {
|
||||
fn default() -> Self {
|
||||
let (sender, receiver) = crossbeam_channel::unbounded();
|
||||
RefChangeChannel { sender, receiver }
|
||||
Handle::Weak(AssetId::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> std::fmt::Debug for Handle<A> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let name = get_short_name(std::any::type_name::<A>());
|
||||
match self {
|
||||
Handle::Strong(handle) => {
|
||||
write!(
|
||||
f,
|
||||
"StrongHandle<{name}>{{ id: {:?}, path: {:?} }}",
|
||||
handle.id.internal(),
|
||||
handle.path
|
||||
)
|
||||
}
|
||||
Handle::Weak(id) => write!(f, "WeakHandle<{name}>({:?})", id.internal()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> Hash for Handle<A> {
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
Hash::hash(&self.id(), state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> PartialOrd for Handle<A> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.id().cmp(&other.id()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> Ord for Handle<A> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.id().cmp(&other.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> PartialEq for Handle<A> {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id() == other.id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> Eq for Handle<A> {}
|
||||
|
||||
/// An untyped variant of [`Handle`], which internally stores the [`Asset`] type information at runtime
|
||||
/// as a [`TypeId`] instead of encoding it in the compile-time type. This allows handles across [`Asset`] types
|
||||
/// to be stored together and compared.
|
||||
///
|
||||
/// See [`Handle`] for more information.
|
||||
#[derive(Clone)]
|
||||
pub enum UntypedHandle {
|
||||
Strong(Arc<StrongHandle>),
|
||||
Weak(UntypedAssetId),
|
||||
}
|
||||
|
||||
impl UntypedHandle {
|
||||
/// Returns the [`UntypedAssetId`] for the referenced asset.
|
||||
#[inline]
|
||||
pub fn id(&self) -> UntypedAssetId {
|
||||
match self {
|
||||
UntypedHandle::Strong(handle) => handle.id,
|
||||
UntypedHandle::Weak(id) => *id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path if this is (1) a strong handle and (2) the asset has a path
|
||||
#[inline]
|
||||
pub fn path(&self) -> Option<&AssetPath<'static>> {
|
||||
match self {
|
||||
UntypedHandle::Strong(handle) => handle.path.as_ref(),
|
||||
UntypedHandle::Weak(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an [`UntypedHandle::Weak`] clone of this [`UntypedHandle`], which will not keep the referenced [`Asset`] alive.
|
||||
#[inline]
|
||||
pub fn clone_weak(&self) -> UntypedHandle {
|
||||
match self {
|
||||
UntypedHandle::Strong(handle) => UntypedHandle::Weak(handle.id),
|
||||
UntypedHandle::Weak(id) => UntypedHandle::Weak(*id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`TypeId`] of the referenced [`Asset`].
|
||||
#[inline]
|
||||
pub fn type_id(&self) -> TypeId {
|
||||
match self {
|
||||
UntypedHandle::Strong(handle) => handle.id.type_id(),
|
||||
UntypedHandle::Weak(id) => id.type_id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts to a typed Handle. This _will not check if the target Handle type matches_.
|
||||
#[inline]
|
||||
pub fn typed_unchecked<A: Asset>(self) -> Handle<A> {
|
||||
match self {
|
||||
UntypedHandle::Strong(handle) => Handle::Strong(handle),
|
||||
UntypedHandle::Weak(id) => Handle::Weak(id.typed_unchecked::<A>()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts to a typed Handle. This will check the type when compiled with debug asserts, but it
|
||||
/// _will not check if the target Handle type matches in release builds_. Use this as an optimization
|
||||
/// when you want some degree of validation at dev-time, but you are also very certain that the type
|
||||
/// actually matches.
|
||||
#[inline]
|
||||
pub fn typed_debug_checked<A: Asset>(self) -> Handle<A> {
|
||||
debug_assert_eq!(
|
||||
self.type_id(),
|
||||
TypeId::of::<A>(),
|
||||
"The target Handle<A>'s TypeId does not match the TypeId of this UntypedHandle"
|
||||
);
|
||||
match self {
|
||||
UntypedHandle::Strong(handle) => Handle::Strong(handle),
|
||||
UntypedHandle::Weak(id) => Handle::Weak(id.typed_unchecked::<A>()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts to a typed Handle. This will panic if the internal [`TypeId`] does not match the given asset type `A`
|
||||
#[inline]
|
||||
pub fn typed<A: Asset>(self) -> Handle<A> {
|
||||
assert_eq!(
|
||||
self.type_id(),
|
||||
TypeId::of::<A>(),
|
||||
"The target Handle<A>'s TypeId does not match the TypeId of this UntypedHandle"
|
||||
);
|
||||
self.typed_unchecked()
|
||||
}
|
||||
|
||||
/// The "meta transform" for the strong handle. This will only be [`Some`] if the handle is strong and there is a meta transform
|
||||
/// associated with it.
|
||||
#[inline]
|
||||
pub fn meta_transform(&self) -> Option<&MetaTransform> {
|
||||
match self {
|
||||
UntypedHandle::Strong(handle) => handle.meta_transform.as_ref(),
|
||||
UntypedHandle::Weak(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for UntypedHandle {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id() == other.id() && self.type_id() == other.type_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for UntypedHandle {}
|
||||
|
||||
impl Hash for UntypedHandle {
|
||||
#[inline]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.id().hash(state);
|
||||
self.type_id().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for UntypedHandle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
UntypedHandle::Strong(handle) => {
|
||||
write!(
|
||||
f,
|
||||
"StrongHandle{{ type_id: {:?}, id: {:?}, path: {:?} }}",
|
||||
handle.id.type_id(),
|
||||
handle.id.internal(),
|
||||
handle.path
|
||||
)
|
||||
}
|
||||
UntypedHandle::Weak(id) => write!(
|
||||
f,
|
||||
"WeakHandle{{ type_id: {:?}, id: {:?} }}",
|
||||
id.type_id(),
|
||||
id.internal()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
377
crates/bevy_asset/src/id.rs
Normal file
377
crates/bevy_asset/src/id.rs
Normal file
|
@ -0,0 +1,377 @@
|
|||
use crate::{Asset, AssetIndex, Handle, UntypedHandle};
|
||||
use bevy_reflect::{Reflect, Uuid};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
fmt::{Debug, Display},
|
||||
hash::Hash,
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
/// A unique runtime-only identifier for an [`Asset`]. This is cheap to [`Copy`]/[`Clone`] and is not directly tied to the
|
||||
/// lifetime of the Asset. This means it _can_ point to an [`Asset`] that no longer exists.
|
||||
///
|
||||
/// For an identifier tied to the lifetime of an asset, see [`Handle`].
|
||||
///
|
||||
/// For an "untyped" / "generic-less" id, see [`UntypedAssetId`].
|
||||
#[derive(Reflect)]
|
||||
pub enum AssetId<A: Asset> {
|
||||
/// A small / efficient runtime identifier that can be used to efficiently look up an asset stored in [`Assets`]. This is
|
||||
/// the "default" identifier used for assets. The alternative(s) (ex: [`AssetId::Uuid`]) will only be used if assets are
|
||||
/// explicitly registered that way.
|
||||
///
|
||||
/// [`Assets`]: crate::Assets
|
||||
Index {
|
||||
index: AssetIndex,
|
||||
#[reflect(ignore)]
|
||||
marker: PhantomData<fn() -> A>,
|
||||
},
|
||||
/// A stable-across-runs / const asset identifier. This will only be used if an asset is explicitly registered in [`Assets`]
|
||||
/// with one.
|
||||
///
|
||||
/// [`Assets`]: crate::Assets
|
||||
Uuid { uuid: Uuid },
|
||||
}
|
||||
|
||||
impl<A: Asset> AssetId<A> {
|
||||
/// The uuid for the default [`AssetId`]. It is valid to assign a value to this in [`Assets`](crate::Assets)
|
||||
/// and by convention (where appropriate) assets should support this pattern.
|
||||
pub const DEFAULT_UUID: Uuid = Uuid::from_u128(200809721996911295814598172825939264631);
|
||||
|
||||
/// This asset id _should_ never be valid. Assigning a value to this in [`Assets`](crate::Assets) will
|
||||
/// produce undefined behavior, so don't do it!
|
||||
pub const INVALID_UUID: Uuid = Uuid::from_u128(108428345662029828789348721013522787528);
|
||||
|
||||
/// Returns an [`AssetId`] with [`Self::INVALID_UUID`], which _should_ never be assigned to.
|
||||
#[inline]
|
||||
pub const fn invalid() -> Self {
|
||||
Self::Uuid {
|
||||
uuid: Self::INVALID_UUID,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts this to an "untyped" / "generic-less" [`Asset`] identifier that stores the type information
|
||||
/// _inside_ the [`UntypedAssetId`].
|
||||
#[inline]
|
||||
pub fn untyped(self) -> UntypedAssetId {
|
||||
match self {
|
||||
AssetId::Index { index, .. } => UntypedAssetId::Index {
|
||||
index,
|
||||
type_id: TypeId::of::<A>(),
|
||||
},
|
||||
AssetId::Uuid { uuid } => UntypedAssetId::Uuid {
|
||||
uuid,
|
||||
type_id: TypeId::of::<A>(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn internal(self) -> InternalAssetId {
|
||||
match self {
|
||||
AssetId::Index { index, .. } => InternalAssetId::Index(index),
|
||||
AssetId::Uuid { uuid } => InternalAssetId::Uuid(uuid),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> Default for AssetId<A> {
|
||||
fn default() -> Self {
|
||||
AssetId::Uuid {
|
||||
uuid: Self::DEFAULT_UUID,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> Clone for AssetId<A> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> Copy for AssetId<A> {}
|
||||
|
||||
impl<A: Asset> Display for AssetId<A> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Debug::fmt(self, f)
|
||||
}
|
||||
}
|
||||
impl<A: Asset> Debug for AssetId<A> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AssetId::Index { index, .. } => {
|
||||
write!(
|
||||
f,
|
||||
"AssetId<{}>{{ index: {}, generation: {}}}",
|
||||
std::any::type_name::<A>(),
|
||||
index.index,
|
||||
index.generation
|
||||
)
|
||||
}
|
||||
AssetId::Uuid { uuid } => {
|
||||
write!(
|
||||
f,
|
||||
"AssetId<{}>{{uuid: {}}}",
|
||||
std::any::type_name::<A>(),
|
||||
uuid
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> Hash for AssetId<A> {
|
||||
#[inline]
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.internal().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> PartialEq for AssetId<A> {
|
||||
#[inline]
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.internal().eq(&other.internal())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> Eq for AssetId<A> {}
|
||||
|
||||
impl<A: Asset> PartialOrd for AssetId<A> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> Ord for AssetId<A> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.internal().cmp(&other.internal())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> From<AssetIndex> for AssetId<A> {
|
||||
#[inline]
|
||||
fn from(value: AssetIndex) -> Self {
|
||||
Self::Index {
|
||||
index: value,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> From<Uuid> for AssetId<A> {
|
||||
#[inline]
|
||||
fn from(value: Uuid) -> Self {
|
||||
Self::Uuid { uuid: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> From<Handle<A>> for AssetId<A> {
|
||||
#[inline]
|
||||
fn from(value: Handle<A>) -> Self {
|
||||
value.id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> From<&Handle<A>> for AssetId<A> {
|
||||
#[inline]
|
||||
fn from(value: &Handle<A>) -> Self {
|
||||
value.id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> From<UntypedHandle> for AssetId<A> {
|
||||
#[inline]
|
||||
fn from(value: UntypedHandle) -> Self {
|
||||
value.id().typed()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> From<&UntypedHandle> for AssetId<A> {
|
||||
#[inline]
|
||||
fn from(value: &UntypedHandle) -> Self {
|
||||
value.id().typed()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> From<UntypedAssetId> for AssetId<A> {
|
||||
#[inline]
|
||||
fn from(value: UntypedAssetId) -> Self {
|
||||
value.typed()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> From<&UntypedAssetId> for AssetId<A> {
|
||||
#[inline]
|
||||
fn from(value: &UntypedAssetId) -> Self {
|
||||
value.typed()
|
||||
}
|
||||
}
|
||||
|
||||
/// An "untyped" / "generic-less" [`Asset`] identifier that behaves much like [`AssetId`], but stores the [`Asset`] type
|
||||
/// information at runtime instead of compile-time. This increases the size of the type, but it enables storing asset ids
|
||||
/// across asset types together and enables comparisons between them.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum UntypedAssetId {
|
||||
/// A small / efficient runtime identifier that can be used to efficiently look up an asset stored in [`Assets`]. This is
|
||||
/// the "default" identifier used for assets. The alternative(s) (ex: [`UntypedAssetId::Uuid`]) will only be used if assets are
|
||||
/// explicitly registered that way.
|
||||
///
|
||||
/// [`Assets`]: crate::Assets
|
||||
Index { type_id: TypeId, index: AssetIndex },
|
||||
/// A stable-across-runs / const asset identifier. This will only be used if an asset is explicitly registered in [`Assets`]
|
||||
/// with one.
|
||||
///
|
||||
/// [`Assets`]: crate::Assets
|
||||
Uuid { type_id: TypeId, uuid: Uuid },
|
||||
}
|
||||
|
||||
impl UntypedAssetId {
|
||||
/// Converts this to a "typed" [`AssetId`] without checking the stored type to see if it matches the target `A` [`Asset`] type.
|
||||
/// This should only be called if you are _absolutely certain_ the asset type matches the stored type. And even then, you should
|
||||
/// consider using [`UntypedAssetId::typed_debug_checked`] instead.
|
||||
#[inline]
|
||||
pub fn typed_unchecked<A: Asset>(self) -> AssetId<A> {
|
||||
match self {
|
||||
UntypedAssetId::Index { index, .. } => AssetId::Index {
|
||||
index,
|
||||
marker: PhantomData,
|
||||
},
|
||||
UntypedAssetId::Uuid { uuid, .. } => AssetId::Uuid { uuid },
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts this to a "typed" [`AssetId`]. When compiled in debug-mode it will check to see if the stored type
|
||||
/// matches the target `A` [`Asset`] type. When compiled in release-mode, this check will be skipped.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if compiled in debug mode and the [`TypeId`] of `A` does not match the stored [`TypeId`].
|
||||
#[inline]
|
||||
pub fn typed_debug_checked<A: Asset>(self) -> AssetId<A> {
|
||||
debug_assert_eq!(
|
||||
self.type_id(),
|
||||
TypeId::of::<A>(),
|
||||
"The target AssetId<{}>'s TypeId does not match the TypeId of this UntypedAssetId",
|
||||
std::any::type_name::<A>()
|
||||
);
|
||||
self.typed_unchecked()
|
||||
}
|
||||
|
||||
/// Converts this to a "typed" [`AssetId`].
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the [`TypeId`] of `A` does not match the stored type id.
|
||||
#[inline]
|
||||
pub fn typed<A: Asset>(self) -> AssetId<A> {
|
||||
assert_eq!(
|
||||
self.type_id(),
|
||||
TypeId::of::<A>(),
|
||||
"The target AssetId<{}>'s TypeId does not match the TypeId of this UntypedAssetId",
|
||||
std::any::type_name::<A>()
|
||||
);
|
||||
self.typed_unchecked()
|
||||
}
|
||||
|
||||
/// Returns the stored [`TypeId`] of the referenced [`Asset`].
|
||||
#[inline]
|
||||
pub fn type_id(&self) -> TypeId {
|
||||
match self {
|
||||
UntypedAssetId::Index { type_id, .. } | UntypedAssetId::Uuid { type_id, .. } => {
|
||||
*type_id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn internal(self) -> InternalAssetId {
|
||||
match self {
|
||||
UntypedAssetId::Index { index, .. } => InternalAssetId::Index(index),
|
||||
UntypedAssetId::Uuid { uuid, .. } => InternalAssetId::Uuid(uuid),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for UntypedAssetId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut writer = f.debug_struct("UntypedAssetId");
|
||||
match self {
|
||||
UntypedAssetId::Index { index, type_id } => {
|
||||
writer
|
||||
.field("type_id", type_id)
|
||||
.field("index", &index.index)
|
||||
.field("generation", &index.generation);
|
||||
}
|
||||
UntypedAssetId::Uuid { uuid, type_id } => {
|
||||
writer.field("type_id", type_id).field("uuid", uuid);
|
||||
}
|
||||
}
|
||||
writer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> From<AssetId<A>> for UntypedAssetId {
|
||||
#[inline]
|
||||
fn from(value: AssetId<A>) -> Self {
|
||||
value.untyped()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> From<Handle<A>> for UntypedAssetId {
|
||||
#[inline]
|
||||
fn from(value: Handle<A>) -> Self {
|
||||
value.id().untyped()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Asset> From<&Handle<A>> for UntypedAssetId {
|
||||
#[inline]
|
||||
fn from(value: &Handle<A>) -> Self {
|
||||
value.id().untyped()
|
||||
}
|
||||
}
|
||||
|
||||
/// An asset id without static or dynamic types associated with it.
|
||||
/// This exist to support efficient type erased id drop tracking. We
|
||||
/// could use [`UntypedAssetId`] for this, but the [`TypeId`] is unnecessary.
|
||||
///
|
||||
/// Do not _ever_ use this across asset types for comparison.
|
||||
/// [`InternalAssetId`] contains no type information and will happily collide
|
||||
/// with indices across types.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub(crate) enum InternalAssetId {
|
||||
Index(AssetIndex),
|
||||
Uuid(Uuid),
|
||||
}
|
||||
|
||||
impl InternalAssetId {
|
||||
#[inline]
|
||||
pub(crate) fn typed<A: Asset>(self) -> AssetId<A> {
|
||||
match self {
|
||||
InternalAssetId::Index(index) => AssetId::Index {
|
||||
index,
|
||||
marker: PhantomData,
|
||||
},
|
||||
InternalAssetId::Uuid(uuid) => AssetId::Uuid { uuid },
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn untyped(self, type_id: TypeId) -> UntypedAssetId {
|
||||
match self {
|
||||
InternalAssetId::Index(index) => UntypedAssetId::Index { index, type_id },
|
||||
InternalAssetId::Uuid(uuid) => UntypedAssetId::Uuid { uuid, type_id },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AssetIndex> for InternalAssetId {
|
||||
fn from(value: AssetIndex) -> Self {
|
||||
Self::Index(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Uuid> for InternalAssetId {
|
||||
fn from(value: Uuid) -> Self {
|
||||
Self::Uuid(value)
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
use crate::{path::AssetPath, LabelId};
|
||||
use bevy_utils::{HashMap, HashSet, Uuid};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Metadata for an asset source.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SourceMeta {
|
||||
/// A collection of asset metadata.
|
||||
pub assets: Vec<AssetMeta>,
|
||||
}
|
||||
|
||||
/// Metadata for an asset.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct AssetMeta {
|
||||
/// Asset label.
|
||||
pub label: Option<String>,
|
||||
/// Asset dependencies.
|
||||
pub dependencies: Vec<AssetPath<'static>>,
|
||||
/// An unique identifier for an asset type.
|
||||
pub type_uuid: Uuid,
|
||||
}
|
||||
|
||||
/// Information about an asset source, such as its path, load state and asset metadata.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SourceInfo {
|
||||
/// Metadata for the source.
|
||||
pub meta: Option<SourceMeta>,
|
||||
/// The path of the source.
|
||||
pub path: PathBuf,
|
||||
/// A map of assets and their type identifiers.
|
||||
pub asset_types: HashMap<LabelId, Uuid>,
|
||||
/// The load state of the source.
|
||||
pub load_state: LoadState,
|
||||
/// A collection to track which assets were sent to their asset storages.
|
||||
pub committed_assets: HashSet<LabelId>,
|
||||
/// Current version of the source.
|
||||
pub version: usize,
|
||||
}
|
||||
|
||||
impl SourceInfo {
|
||||
/// Returns `true` if all assets tracked by the source were loaded into their asset storages.
|
||||
pub fn is_loaded(&self) -> bool {
|
||||
self.meta.as_ref().map_or(false, |meta| {
|
||||
self.committed_assets.len() == meta.assets.len()
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the type identifier for an asset identified by `label_id`.
|
||||
pub fn get_asset_type(&self, label_id: LabelId) -> Option<Uuid> {
|
||||
self.asset_types.get(&label_id).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
/// The load state of an asset.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum LoadState {
|
||||
/// The asset has not been loaded.
|
||||
NotLoaded,
|
||||
/// The asset is in the process of loading.
|
||||
Loading,
|
||||
/// The asset has been loaded and is living inside an [`Assets`](crate::Assets) collection.
|
||||
Loaded,
|
||||
/// The asset failed to load.
|
||||
Failed,
|
||||
/// The asset was previously loaded, however all handles were dropped and the asset was removed
|
||||
/// from the [`Assets`](crate::Assets) collection.
|
||||
Unloaded,
|
||||
}
|
82
crates/bevy_asset/src/io/android.rs
Normal file
82
crates/bevy_asset/src/io/android.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use crate::io::{
|
||||
get_meta_path, AssetReader, AssetReaderError, AssetWatcher, EmptyPathStream, PathStream,
|
||||
Reader, VecReader,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use bevy_log::error;
|
||||
use bevy_utils::BoxedFuture;
|
||||
use std::{ffi::CString, path::Path};
|
||||
|
||||
/// [`AssetReader`] implementation for Android devices, built on top of Android's [`AssetManager`].
|
||||
///
|
||||
/// Implementation details:
|
||||
///
|
||||
/// - [`load_path`](AssetIo::load_path) uses the [`AssetManager`] to load files.
|
||||
/// - [`read_directory`](AssetIo::read_directory) always returns an empty iterator.
|
||||
/// - Watching for changes is not supported. The watcher method will do nothing.
|
||||
///
|
||||
/// [AssetManager]: https://developer.android.com/reference/android/content/res/AssetManager
|
||||
pub struct AndroidAssetReader;
|
||||
|
||||
impl AssetReader for AndroidAssetReader {
|
||||
fn read<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
let asset_manager = bevy_winit::ANDROID_APP
|
||||
.get()
|
||||
.expect("Bevy must be setup with the #[bevy_main] macro on Android")
|
||||
.asset_manager();
|
||||
let mut opened_asset = asset_manager
|
||||
.open(&CString::new(path.to_str().unwrap()).unwrap())
|
||||
.ok_or(AssetReaderError::NotFound(path.to_path_buf()))?;
|
||||
let bytes = opened_asset.get_buffer()?;
|
||||
let reader: Box<Reader> = Box::new(VecReader::new(bytes.to_vec()));
|
||||
Ok(reader)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_meta<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
let meta_path = get_meta_path(path);
|
||||
let asset_manager = bevy_winit::ANDROID_APP
|
||||
.get()
|
||||
.expect("Bevy must be setup with the #[bevy_main] macro on Android")
|
||||
.asset_manager();
|
||||
let mut opened_asset = asset_manager
|
||||
.open(&CString::new(meta_path.to_str().unwrap()).unwrap())
|
||||
.ok_or(AssetReaderError::NotFound(meta_path))?;
|
||||
let bytes = opened_asset.get_buffer()?;
|
||||
let reader: Box<Reader> = Box::new(VecReader::new(bytes.to_vec()));
|
||||
Ok(reader)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
_path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>> {
|
||||
let stream: Box<PathStream> = Box::new(EmptyPathStream);
|
||||
error!("Reading directories is not supported with the AndroidAssetReader");
|
||||
Box::pin(async move { Ok(stream) })
|
||||
}
|
||||
|
||||
fn is_directory<'a>(
|
||||
&'a self,
|
||||
_path: &'a Path,
|
||||
) -> BoxedFuture<'a, std::result::Result<bool, AssetReaderError>> {
|
||||
error!("Reading directories is not supported with the AndroidAssetReader");
|
||||
Box::pin(async move { Ok(false) })
|
||||
}
|
||||
|
||||
fn watch_for_changes(
|
||||
&self,
|
||||
_event_sender: crossbeam_channel::Sender<super::AssetSourceEvent>,
|
||||
) -> Option<Box<dyn AssetWatcher>> {
|
||||
None
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
use crate::{AssetIo, AssetIoError, ChangeWatcher, Metadata};
|
||||
use anyhow::Result;
|
||||
use bevy_utils::BoxedFuture;
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
ffi::CString,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// I/O implementation for Android devices.
|
||||
///
|
||||
/// Implementation details:
|
||||
///
|
||||
/// - [`load_path`](AssetIo::load_path) uses the [`AssetManager`] to load files.
|
||||
/// - [`read_directory`](AssetIo::read_directory) always returns an empty iterator.
|
||||
/// - [`get_metadata`](AssetIo::get_metadata) will probably return an error.
|
||||
/// - Watching for changes is not supported. The watcher methods will do nothing.
|
||||
///
|
||||
/// [AssetManager]: https://developer.android.com/reference/android/content/res/AssetManager
|
||||
pub struct AndroidAssetIo {
|
||||
root_path: PathBuf,
|
||||
}
|
||||
|
||||
impl AndroidAssetIo {
|
||||
/// Creates a new [`AndroidAssetIo`] at the given root path
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
||||
AndroidAssetIo {
|
||||
root_path: path.as_ref().to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetIo for AndroidAssetIo {
|
||||
fn load_path<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<Vec<u8>, AssetIoError>> {
|
||||
Box::pin(async move {
|
||||
let asset_manager = bevy_winit::ANDROID_APP
|
||||
.get()
|
||||
.expect("Bevy must be setup with the #[bevy_main] macro on Android")
|
||||
.asset_manager();
|
||||
let mut opened_asset = asset_manager
|
||||
.open(&CString::new(path.to_str().unwrap()).unwrap())
|
||||
.ok_or(AssetIoError::NotFound(path.to_path_buf()))?;
|
||||
let bytes = opened_asset.get_buffer()?;
|
||||
Ok(bytes.to_vec())
|
||||
})
|
||||
}
|
||||
|
||||
fn read_directory(
|
||||
&self,
|
||||
_path: &Path,
|
||||
) -> Result<Box<dyn Iterator<Item = PathBuf>>, AssetIoError> {
|
||||
Ok(Box::new(std::iter::empty::<PathBuf>()))
|
||||
}
|
||||
|
||||
fn watch_path_for_changes(
|
||||
&self,
|
||||
_to_watch: &Path,
|
||||
_to_reload: Option<PathBuf>,
|
||||
) -> Result<(), AssetIoError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn watch_for_changes(&self, _configuration: &ChangeWatcher) -> Result<(), AssetIoError> {
|
||||
bevy_log::warn!("Watching for changes is not supported on Android");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_metadata(&self, path: &Path) -> Result<Metadata, AssetIoError> {
|
||||
let full_path = self.root_path.join(path);
|
||||
full_path
|
||||
.metadata()
|
||||
.and_then(Metadata::try_from)
|
||||
.map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
AssetIoError::NotFound(full_path)
|
||||
} else {
|
||||
e.into()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
182
crates/bevy_asset/src/io/file/file_watcher.rs
Normal file
182
crates/bevy_asset/src/io/file/file_watcher.rs
Normal file
|
@ -0,0 +1,182 @@
|
|||
use crate::io::{AssetSourceEvent, AssetWatcher};
|
||||
use anyhow::Result;
|
||||
use bevy_log::error;
|
||||
use bevy_utils::Duration;
|
||||
use crossbeam_channel::Sender;
|
||||
use notify_debouncer_full::{
|
||||
new_debouncer,
|
||||
notify::{
|
||||
self,
|
||||
event::{AccessKind, AccessMode, CreateKind, ModifyKind, RemoveKind, RenameMode},
|
||||
RecommendedWatcher, RecursiveMode, Watcher,
|
||||
},
|
||||
DebounceEventResult, Debouncer, FileIdMap,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub struct FileWatcher {
|
||||
_watcher: Debouncer<RecommendedWatcher, FileIdMap>,
|
||||
}
|
||||
|
||||
impl FileWatcher {
|
||||
pub fn new(
|
||||
root: PathBuf,
|
||||
sender: Sender<AssetSourceEvent>,
|
||||
debounce_wait_time: Duration,
|
||||
) -> Result<Self, notify::Error> {
|
||||
let owned_root = root.clone();
|
||||
let mut debouncer = new_debouncer(
|
||||
debounce_wait_time,
|
||||
None,
|
||||
move |result: DebounceEventResult| {
|
||||
match result {
|
||||
Ok(events) => {
|
||||
for event in events.iter() {
|
||||
match event.kind {
|
||||
notify::EventKind::Create(CreateKind::File) => {
|
||||
let (path, is_meta) =
|
||||
get_asset_path(&owned_root, &event.paths[0]);
|
||||
if is_meta {
|
||||
sender.send(AssetSourceEvent::AddedMeta(path)).unwrap();
|
||||
} else {
|
||||
sender.send(AssetSourceEvent::AddedAsset(path)).unwrap();
|
||||
}
|
||||
}
|
||||
notify::EventKind::Create(CreateKind::Folder) => {
|
||||
let (path, _) = get_asset_path(&owned_root, &event.paths[0]);
|
||||
sender.send(AssetSourceEvent::AddedFolder(path)).unwrap();
|
||||
}
|
||||
notify::EventKind::Modify(ModifyKind::Any) => {
|
||||
let (path, is_meta) =
|
||||
get_asset_path(&owned_root, &event.paths[0]);
|
||||
if event.paths[0].is_dir() {
|
||||
// modified folder means nothing in this case
|
||||
} else if is_meta {
|
||||
sender.send(AssetSourceEvent::ModifiedMeta(path)).unwrap();
|
||||
} else {
|
||||
sender.send(AssetSourceEvent::ModifiedAsset(path)).unwrap();
|
||||
};
|
||||
}
|
||||
notify::EventKind::Access(AccessKind::Close(AccessMode::Write)) => {
|
||||
let (path, is_meta) =
|
||||
get_asset_path(&owned_root, &event.paths[0]);
|
||||
if is_meta {
|
||||
sender.send(AssetSourceEvent::ModifiedMeta(path)).unwrap();
|
||||
} else {
|
||||
sender.send(AssetSourceEvent::ModifiedAsset(path)).unwrap();
|
||||
}
|
||||
}
|
||||
notify::EventKind::Remove(RemoveKind::Any) |
|
||||
// Because this is debounced over a reasonable period of time, "From" events are assumed to be "dangling" without
|
||||
// a follow up "To" event. Without debouncing, "From" -> "To" -> "Both" events are emitted for renames.
|
||||
// If a From is dangling, it is assumed to be "removed" from the context of the asset system.
|
||||
notify::EventKind::Modify(ModifyKind::Name(RenameMode::From)) => {
|
||||
let (path, is_meta) =
|
||||
get_asset_path(&owned_root, &event.paths[0]);
|
||||
sender
|
||||
.send(AssetSourceEvent::RemovedUnknown { path, is_meta })
|
||||
.unwrap();
|
||||
}
|
||||
notify::EventKind::Create(CreateKind::Any)
|
||||
| notify::EventKind::Modify(ModifyKind::Name(RenameMode::To)) => {
|
||||
let (path, is_meta) =
|
||||
get_asset_path(&owned_root, &event.paths[0]);
|
||||
let event = if event.paths[0].is_dir() {
|
||||
AssetSourceEvent::AddedFolder(path)
|
||||
} else if is_meta {
|
||||
AssetSourceEvent::AddedMeta(path)
|
||||
} else {
|
||||
AssetSourceEvent::AddedAsset(path)
|
||||
};
|
||||
sender.send(event).unwrap();
|
||||
}
|
||||
notify::EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
|
||||
let (old_path, old_is_meta) =
|
||||
get_asset_path(&owned_root, &event.paths[0]);
|
||||
let (new_path, new_is_meta) =
|
||||
get_asset_path(&owned_root, &event.paths[1]);
|
||||
// only the new "real" path is considered a directory
|
||||
if event.paths[1].is_dir() {
|
||||
sender
|
||||
.send(AssetSourceEvent::RenamedFolder {
|
||||
old: old_path,
|
||||
new: new_path,
|
||||
})
|
||||
.unwrap();
|
||||
} else {
|
||||
match (old_is_meta, new_is_meta) {
|
||||
(true, true) => {
|
||||
sender
|
||||
.send(AssetSourceEvent::RenamedMeta {
|
||||
old: old_path,
|
||||
new: new_path,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
(false, false) => {
|
||||
sender
|
||||
.send(AssetSourceEvent::RenamedAsset {
|
||||
old: old_path,
|
||||
new: new_path,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
(true, false) => {
|
||||
error!(
|
||||
"Asset metafile {old_path:?} was changed to asset file {new_path:?}, which is not supported. Try restarting your app to see if configuration is still valid"
|
||||
);
|
||||
}
|
||||
(false, true) => {
|
||||
error!(
|
||||
"Asset file {old_path:?} was changed to meta file {new_path:?}, which is not supported. Try restarting your app to see if configuration is still valid"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
notify::EventKind::Remove(RemoveKind::File) => {
|
||||
let (path, is_meta) =
|
||||
get_asset_path(&owned_root, &event.paths[0]);
|
||||
if is_meta {
|
||||
sender.send(AssetSourceEvent::RemovedMeta(path)).unwrap();
|
||||
} else {
|
||||
sender.send(AssetSourceEvent::RemovedAsset(path)).unwrap();
|
||||
}
|
||||
}
|
||||
notify::EventKind::Remove(RemoveKind::Folder) => {
|
||||
let (path, _) = get_asset_path(&owned_root, &event.paths[0]);
|
||||
sender.send(AssetSourceEvent::RemovedFolder(path)).unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(errors) => errors.iter().for_each(|error| {
|
||||
error!("Encountered a filesystem watcher error {error:?}");
|
||||
}),
|
||||
}
|
||||
},
|
||||
)?;
|
||||
debouncer.watcher().watch(&root, RecursiveMode::Recursive)?;
|
||||
debouncer.cache().add_root(&root, RecursiveMode::Recursive);
|
||||
Ok(Self {
|
||||
_watcher: debouncer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetWatcher for FileWatcher {}
|
||||
|
||||
pub(crate) fn get_asset_path(root: &Path, absolute_path: &Path) -> (PathBuf, bool) {
|
||||
let relative_path = absolute_path.strip_prefix(root).unwrap();
|
||||
let is_meta = relative_path
|
||||
.extension()
|
||||
.map(|e| e == "meta")
|
||||
.unwrap_or(false);
|
||||
let asset_path = if is_meta {
|
||||
relative_path.with_extension("")
|
||||
} else {
|
||||
relative_path.to_owned()
|
||||
};
|
||||
(asset_path, is_meta)
|
||||
}
|
325
crates/bevy_asset/src/io/file/mod.rs
Normal file
325
crates/bevy_asset/src/io/file/mod.rs
Normal file
|
@ -0,0 +1,325 @@
|
|||
#[cfg(feature = "filesystem_watcher")]
|
||||
mod file_watcher;
|
||||
|
||||
use crate::io::{
|
||||
get_meta_path, AssetReader, AssetReaderError, AssetWatcher, AssetWriter, AssetWriterError,
|
||||
PathStream, Reader, Writer,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_fs::{read_dir, File};
|
||||
use bevy_utils::BoxedFuture;
|
||||
use futures_lite::StreamExt;
|
||||
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
pub(crate) fn get_base_path() -> PathBuf {
|
||||
if let Ok(manifest_dir) = env::var("BEVY_ASSET_ROOT") {
|
||||
PathBuf::from(manifest_dir)
|
||||
} else if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
|
||||
PathBuf::from(manifest_dir)
|
||||
} else {
|
||||
env::current_exe()
|
||||
.map(|path| {
|
||||
path.parent()
|
||||
.map(|exe_parent_path| exe_parent_path.to_owned())
|
||||
.unwrap()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// I/O implementation for the local filesystem.
|
||||
///
|
||||
/// This asset I/O is fully featured but it's not available on `android` and `wasm` targets.
|
||||
pub struct FileAssetReader {
|
||||
root_path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileAssetReader {
|
||||
/// Creates a new `FileAssetIo` at a path relative to the executable's directory, optionally
|
||||
/// watching for changes.
|
||||
///
|
||||
/// See `get_base_path` below.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
||||
let root_path = Self::get_base_path().join(path.as_ref());
|
||||
std::fs::create_dir_all(&root_path).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"Failed to create root directory {:?} for file asset reader: {:?}",
|
||||
root_path, e
|
||||
)
|
||||
});
|
||||
Self { root_path }
|
||||
}
|
||||
|
||||
/// Returns the base path of the assets directory, which is normally the executable's parent
|
||||
/// directory.
|
||||
///
|
||||
/// If the `CARGO_MANIFEST_DIR` environment variable is set, then its value will be used
|
||||
/// instead. It's set by cargo when running with `cargo run`.
|
||||
pub fn get_base_path() -> PathBuf {
|
||||
get_base_path()
|
||||
}
|
||||
|
||||
/// Returns the root directory where assets are loaded from.
|
||||
///
|
||||
/// See `get_base_path`.
|
||||
pub fn root_path(&self) -> &PathBuf {
|
||||
&self.root_path
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetReader for FileAssetReader {
|
||||
fn read<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
let full_path = self.root_path.join(path);
|
||||
match File::open(&full_path).await {
|
||||
Ok(file) => {
|
||||
let reader: Box<Reader> = Box::new(file);
|
||||
Ok(reader)
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
Err(AssetReaderError::NotFound(full_path))
|
||||
} else {
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn read_meta<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
let meta_path = get_meta_path(path);
|
||||
Box::pin(async move {
|
||||
let full_path = self.root_path.join(meta_path);
|
||||
match File::open(&full_path).await {
|
||||
Ok(file) => {
|
||||
let reader: Box<Reader> = Box::new(file);
|
||||
Ok(reader)
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
Err(AssetReaderError::NotFound(full_path))
|
||||
} else {
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
let full_path = self.root_path.join(path);
|
||||
match read_dir(&full_path).await {
|
||||
Ok(read_dir) => {
|
||||
let root_path = self.root_path.clone();
|
||||
let mapped_stream = read_dir.filter_map(move |f| {
|
||||
f.ok().and_then(|dir_entry| {
|
||||
let path = dir_entry.path();
|
||||
// filter out meta files as they are not considered assets
|
||||
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
||||
if ext.eq_ignore_ascii_case("meta") {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let relative_path = path.strip_prefix(&root_path).unwrap();
|
||||
Some(relative_path.to_owned())
|
||||
})
|
||||
});
|
||||
let read_dir: Box<PathStream> = Box::new(mapped_stream);
|
||||
Ok(read_dir)
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
Err(AssetReaderError::NotFound(full_path))
|
||||
} else {
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn is_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, std::result::Result<bool, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
let full_path = self.root_path.join(path);
|
||||
let metadata = full_path
|
||||
.metadata()
|
||||
.map_err(|_e| AssetReaderError::NotFound(path.to_owned()))?;
|
||||
Ok(metadata.file_type().is_dir())
|
||||
})
|
||||
}
|
||||
|
||||
fn watch_for_changes(
|
||||
&self,
|
||||
_event_sender: crossbeam_channel::Sender<super::AssetSourceEvent>,
|
||||
) -> Option<Box<dyn AssetWatcher>> {
|
||||
#[cfg(feature = "filesystem_watcher")]
|
||||
return Some(Box::new(
|
||||
file_watcher::FileWatcher::new(
|
||||
self.root_path.clone(),
|
||||
_event_sender,
|
||||
std::time::Duration::from_millis(300),
|
||||
)
|
||||
.unwrap(),
|
||||
));
|
||||
#[cfg(not(feature = "filesystem_watcher"))]
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileAssetWriter {
|
||||
root_path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileAssetWriter {
|
||||
/// Creates a new `FileAssetIo` at a path relative to the executable's directory, optionally
|
||||
/// watching for changes.
|
||||
///
|
||||
/// See `get_base_path` below.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
||||
Self {
|
||||
root_path: get_base_path().join(path.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetWriter for FileAssetWriter {
|
||||
fn write<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>> {
|
||||
Box::pin(async move {
|
||||
let full_path = self.root_path.join(path);
|
||||
if let Some(parent) = full_path.parent() {
|
||||
async_fs::create_dir_all(parent).await?;
|
||||
}
|
||||
let file = File::create(&full_path).await?;
|
||||
let writer: Box<Writer> = Box::new(file);
|
||||
Ok(writer)
|
||||
})
|
||||
}
|
||||
|
||||
fn write_meta<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>> {
|
||||
Box::pin(async move {
|
||||
let meta_path = get_meta_path(path);
|
||||
let full_path = self.root_path.join(meta_path);
|
||||
if let Some(parent) = full_path.parent() {
|
||||
async_fs::create_dir_all(parent).await?;
|
||||
}
|
||||
let file = File::create(&full_path).await?;
|
||||
let writer: Box<Writer> = Box::new(file);
|
||||
Ok(writer)
|
||||
})
|
||||
}
|
||||
|
||||
fn remove<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> {
|
||||
Box::pin(async move {
|
||||
let full_path = self.root_path.join(path);
|
||||
async_fs::remove_file(full_path).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_meta<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> {
|
||||
Box::pin(async move {
|
||||
let meta_path = get_meta_path(path);
|
||||
let full_path = self.root_path.join(meta_path);
|
||||
async_fs::remove_file(full_path).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> {
|
||||
Box::pin(async move {
|
||||
let full_path = self.root_path.join(path);
|
||||
async_fs::remove_dir_all(full_path).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_empty_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> {
|
||||
Box::pin(async move {
|
||||
let full_path = self.root_path.join(path);
|
||||
async_fs::remove_dir(full_path).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_assets_in_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> {
|
||||
Box::pin(async move {
|
||||
let full_path = self.root_path.join(path);
|
||||
async_fs::remove_dir_all(&full_path).await?;
|
||||
async_fs::create_dir_all(&full_path).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn rename<'a>(
|
||||
&'a self,
|
||||
old_path: &'a Path,
|
||||
new_path: &'a Path,
|
||||
) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> {
|
||||
Box::pin(async move {
|
||||
let full_old_path = self.root_path.join(old_path);
|
||||
let full_new_path = self.root_path.join(new_path);
|
||||
if let Some(parent) = full_new_path.parent() {
|
||||
async_fs::create_dir_all(parent).await?;
|
||||
}
|
||||
async_fs::rename(full_old_path, full_new_path).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn rename_meta<'a>(
|
||||
&'a self,
|
||||
old_path: &'a Path,
|
||||
new_path: &'a Path,
|
||||
) -> BoxedFuture<'a, std::result::Result<(), AssetWriterError>> {
|
||||
Box::pin(async move {
|
||||
let old_meta_path = get_meta_path(old_path);
|
||||
let new_meta_path = get_meta_path(new_path);
|
||||
let full_old_path = self.root_path.join(old_meta_path);
|
||||
let full_new_path = self.root_path.join(new_meta_path);
|
||||
if let Some(parent) = full_new_path.parent() {
|
||||
async_fs::create_dir_all(parent).await?;
|
||||
}
|
||||
async_fs::rename(full_old_path, full_new_path).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,227 +0,0 @@
|
|||
#[cfg(feature = "filesystem_watcher")]
|
||||
use crate::{filesystem_watcher::FilesystemWatcher, AssetServer};
|
||||
use crate::{AssetIo, AssetIoError, ChangeWatcher, Metadata};
|
||||
use anyhow::Result;
|
||||
#[cfg(feature = "filesystem_watcher")]
|
||||
use bevy_ecs::system::{Local, Res};
|
||||
use bevy_utils::BoxedFuture;
|
||||
#[cfg(feature = "filesystem_watcher")]
|
||||
use bevy_utils::{default, HashMap, Instant};
|
||||
#[cfg(feature = "filesystem_watcher")]
|
||||
use crossbeam_channel::TryRecvError;
|
||||
use fs::File;
|
||||
#[cfg(feature = "filesystem_watcher")]
|
||||
use parking_lot::RwLock;
|
||||
#[cfg(feature = "filesystem_watcher")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
env, fs,
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// I/O implementation for the local filesystem.
|
||||
///
|
||||
/// This asset I/O is fully featured but it's not available on `android` and `wasm` targets.
|
||||
pub struct FileAssetIo {
|
||||
root_path: PathBuf,
|
||||
#[cfg(feature = "filesystem_watcher")]
|
||||
filesystem_watcher: Arc<RwLock<Option<FilesystemWatcher>>>,
|
||||
}
|
||||
|
||||
impl FileAssetIo {
|
||||
/// Creates a new `FileAssetIo` at a path relative to the executable's directory, optionally
|
||||
/// watching for changes.
|
||||
///
|
||||
/// See `get_base_path` below.
|
||||
pub fn new<P: AsRef<Path>>(path: P, watch_for_changes: &Option<ChangeWatcher>) -> Self {
|
||||
let file_asset_io = FileAssetIo {
|
||||
#[cfg(feature = "filesystem_watcher")]
|
||||
filesystem_watcher: default(),
|
||||
root_path: Self::get_base_path().join(path.as_ref()),
|
||||
};
|
||||
if let Some(configuration) = watch_for_changes {
|
||||
#[cfg(any(
|
||||
not(feature = "filesystem_watcher"),
|
||||
target_arch = "wasm32",
|
||||
target_os = "android"
|
||||
))]
|
||||
panic!(
|
||||
"Watch for changes requires the filesystem_watcher feature and cannot be used on \
|
||||
wasm32 / android targets"
|
||||
);
|
||||
#[cfg(feature = "filesystem_watcher")]
|
||||
file_asset_io.watch_for_changes(configuration).unwrap();
|
||||
}
|
||||
file_asset_io
|
||||
}
|
||||
|
||||
/// Returns the base path of the assets directory, which is normally the executable's parent
|
||||
/// directory.
|
||||
///
|
||||
/// If a `BEVY_ASSET_ROOT` environment variable is set, then its value will be used.
|
||||
///
|
||||
/// Else if the `CARGO_MANIFEST_DIR` environment variable is set, then its value will be used
|
||||
/// instead. It's set by cargo when running with `cargo run`.
|
||||
pub fn get_base_path() -> PathBuf {
|
||||
if let Ok(env_bevy_asset_root) = env::var("BEVY_ASSET_ROOT") {
|
||||
PathBuf::from(env_bevy_asset_root)
|
||||
} else if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
|
||||
PathBuf::from(manifest_dir)
|
||||
} else {
|
||||
env::current_exe()
|
||||
.map(|path| {
|
||||
path.parent()
|
||||
.map(|exe_parent_path| exe_parent_path.to_owned())
|
||||
.unwrap()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the root directory where assets are loaded from.
|
||||
///
|
||||
/// See [`get_base_path`](FileAssetIo::get_base_path).
|
||||
pub fn root_path(&self) -> &PathBuf {
|
||||
&self.root_path
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetIo for FileAssetIo {
|
||||
fn load_path<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<Vec<u8>, AssetIoError>> {
|
||||
Box::pin(async move {
|
||||
let mut bytes = Vec::new();
|
||||
let full_path = self.root_path.join(path);
|
||||
match File::open(&full_path) {
|
||||
Ok(mut file) => {
|
||||
file.read_to_end(&mut bytes)?;
|
||||
}
|
||||
Err(e) => {
|
||||
return if e.kind() == std::io::ErrorKind::NotFound {
|
||||
Err(AssetIoError::NotFound(full_path))
|
||||
} else {
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(bytes)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_directory(
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> Result<Box<dyn Iterator<Item = PathBuf>>, AssetIoError> {
|
||||
let root_path = self.root_path.to_owned();
|
||||
let path = path.to_owned();
|
||||
Ok(Box::new(fs::read_dir(root_path.join(&path))?.map(
|
||||
move |entry| {
|
||||
let file_name = entry.unwrap().file_name();
|
||||
path.join(file_name)
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
fn watch_path_for_changes(
|
||||
&self,
|
||||
to_watch: &Path,
|
||||
to_reload: Option<PathBuf>,
|
||||
) -> Result<(), AssetIoError> {
|
||||
#![allow(unused_variables)]
|
||||
#[cfg(feature = "filesystem_watcher")]
|
||||
{
|
||||
let to_reload = to_reload.unwrap_or_else(|| to_watch.to_owned());
|
||||
let to_watch = self.root_path.join(to_watch);
|
||||
let mut watcher = self.filesystem_watcher.write();
|
||||
if let Some(ref mut watcher) = *watcher {
|
||||
watcher
|
||||
.watch(&to_watch, to_reload)
|
||||
.map_err(|_error| AssetIoError::PathWatchError(to_watch))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn watch_for_changes(&self, configuration: &ChangeWatcher) -> Result<(), AssetIoError> {
|
||||
#[cfg(feature = "filesystem_watcher")]
|
||||
{
|
||||
*self.filesystem_watcher.write() = Some(FilesystemWatcher::new(configuration));
|
||||
}
|
||||
#[cfg(not(feature = "filesystem_watcher"))]
|
||||
bevy_log::warn!("Watching for changes is not supported when the `filesystem_watcher` feature is disabled");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_metadata(&self, path: &Path) -> Result<Metadata, AssetIoError> {
|
||||
let full_path = self.root_path.join(path);
|
||||
full_path
|
||||
.metadata()
|
||||
.and_then(Metadata::try_from)
|
||||
.map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
AssetIoError::NotFound(full_path)
|
||||
} else {
|
||||
e.into()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Watches for file changes in the local file system.
|
||||
#[cfg(all(
|
||||
feature = "filesystem_watcher",
|
||||
all(not(target_arch = "wasm32"), not(target_os = "android"))
|
||||
))]
|
||||
pub fn filesystem_watcher_system(
|
||||
asset_server: Res<AssetServer>,
|
||||
mut changed: Local<HashMap<PathBuf, Instant>>,
|
||||
) {
|
||||
let asset_io =
|
||||
if let Some(asset_io) = asset_server.server.asset_io.downcast_ref::<FileAssetIo>() {
|
||||
asset_io
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let watcher = asset_io.filesystem_watcher.read();
|
||||
|
||||
if let Some(ref watcher) = *watcher {
|
||||
loop {
|
||||
let event = match watcher.receiver.try_recv() {
|
||||
Ok(result) => result.unwrap(),
|
||||
Err(TryRecvError::Empty) => break,
|
||||
Err(TryRecvError::Disconnected) => panic!("FilesystemWatcher disconnected."),
|
||||
};
|
||||
|
||||
if let notify::event::Event {
|
||||
kind: notify::event::EventKind::Modify(_),
|
||||
paths,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
for path in &paths {
|
||||
let Some(set) = watcher.path_map.get(path) else {
|
||||
continue;
|
||||
};
|
||||
for to_reload in set {
|
||||
// When an asset is modified, note down the timestamp (overriding any previous modification events)
|
||||
changed.insert(to_reload.to_owned(), Instant::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reload all assets whose last modification was at least 50ms ago.
|
||||
//
|
||||
// When changing and then saving a shader, several modification events are sent in short succession.
|
||||
// Unless we wait until we are sure the shader is finished being modified (and that there will be no more events coming),
|
||||
// we will sometimes get a crash when trying to reload a partially-modified shader.
|
||||
for (to_reload, _) in
|
||||
changed.extract_if(|_, last_modified| last_modified.elapsed() >= watcher.delay)
|
||||
{
|
||||
let _ = asset_server.load_untracked(to_reload.as_path().into(), true);
|
||||
}
|
||||
}
|
||||
}
|
107
crates/bevy_asset/src/io/gated.rs
Normal file
107
crates/bevy_asset/src/io/gated.rs
Normal file
|
@ -0,0 +1,107 @@
|
|||
use crate::io::{AssetReader, AssetReaderError, PathStream, Reader};
|
||||
use anyhow::Result;
|
||||
use bevy_utils::{BoxedFuture, HashMap};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use parking_lot::RwLock;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
/// A "gated" reader that will prevent asset reads from returning until
|
||||
/// a given path has been "opened" using [`GateOpener`].
|
||||
///
|
||||
/// This is built primarily for unit tests.
|
||||
pub struct GatedReader<R: AssetReader> {
|
||||
reader: R,
|
||||
gates: Arc<RwLock<HashMap<PathBuf, (Sender<()>, Receiver<()>)>>>,
|
||||
}
|
||||
|
||||
impl<R: AssetReader + Clone> Clone for GatedReader<R> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
reader: self.reader.clone(),
|
||||
gates: self.gates.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens path "gates" for a [`GatedReader`].
|
||||
pub struct GateOpener {
|
||||
gates: Arc<RwLock<HashMap<PathBuf, (Sender<()>, Receiver<()>)>>>,
|
||||
}
|
||||
|
||||
impl GateOpener {
|
||||
/// Opens the `path` "gate", allowing a _single_ [`AssetReader`] operation to return for that path.
|
||||
/// If multiple operations are expected, call `open` the expected number of calls.
|
||||
pub fn open<P: AsRef<Path>>(&self, path: P) {
|
||||
let mut gates = self.gates.write();
|
||||
let gates = gates
|
||||
.entry(path.as_ref().to_path_buf())
|
||||
.or_insert_with(crossbeam_channel::unbounded);
|
||||
gates.0.send(()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AssetReader> GatedReader<R> {
|
||||
/// Creates a new [`GatedReader`], which wraps the given `reader`. Also returns a [`GateOpener`] which
|
||||
/// can be used to open "path gates" for this [`GatedReader`].
|
||||
pub fn new(reader: R) -> (Self, GateOpener) {
|
||||
let gates = Arc::new(RwLock::new(HashMap::new()));
|
||||
(
|
||||
Self {
|
||||
reader,
|
||||
gates: gates.clone(),
|
||||
},
|
||||
GateOpener { gates },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: AssetReader> AssetReader for GatedReader<R> {
|
||||
fn read<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
let receiver = {
|
||||
let mut gates = self.gates.write();
|
||||
let gates = gates
|
||||
.entry(path.to_path_buf())
|
||||
.or_insert_with(crossbeam_channel::unbounded);
|
||||
gates.1.clone()
|
||||
};
|
||||
Box::pin(async move {
|
||||
receiver.recv().unwrap();
|
||||
let result = self.reader.read(path).await?;
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_meta<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
self.reader.read_meta(path)
|
||||
}
|
||||
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>> {
|
||||
self.reader.read_directory(path)
|
||||
}
|
||||
|
||||
fn is_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, std::result::Result<bool, AssetReaderError>> {
|
||||
self.reader.is_directory(path)
|
||||
}
|
||||
|
||||
fn watch_for_changes(
|
||||
&self,
|
||||
event_sender: Sender<super::AssetSourceEvent>,
|
||||
) -> Option<Box<dyn super::AssetWatcher>> {
|
||||
self.reader.watch_for_changes(event_sender)
|
||||
}
|
||||
}
|
288
crates/bevy_asset/src/io/memory.rs
Normal file
288
crates/bevy_asset/src/io/memory.rs
Normal file
|
@ -0,0 +1,288 @@
|
|||
use crate::io::{AssetReader, AssetReaderError, PathStream, Reader};
|
||||
use anyhow::Result;
|
||||
use bevy_utils::{BoxedFuture, HashMap};
|
||||
use futures_io::AsyncRead;
|
||||
use futures_lite::{ready, Stream};
|
||||
use parking_lot::RwLock;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
task::Poll,
|
||||
};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct DirInternal {
|
||||
assets: HashMap<String, Data>,
|
||||
metadata: HashMap<String, Data>,
|
||||
dirs: HashMap<String, Dir>,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
/// A clone-able (internally Arc-ed) / thread-safe "in memory" filesystem.
|
||||
/// This is built for [`MemoryAssetReader`] and is primarily intended for unit tests.
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct Dir(Arc<RwLock<DirInternal>>);
|
||||
|
||||
impl Dir {
|
||||
/// Creates a new [`Dir`] for the given `path`.
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self(Arc::new(RwLock::new(DirInternal {
|
||||
path,
|
||||
..Default::default()
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn insert_asset_text(&self, path: &Path, asset: &str) {
|
||||
self.insert_asset(path, asset.as_bytes().to_vec());
|
||||
}
|
||||
|
||||
pub fn insert_meta_text(&self, path: &Path, asset: &str) {
|
||||
self.insert_meta(path, asset.as_bytes().to_vec());
|
||||
}
|
||||
|
||||
pub fn insert_asset(&self, path: &Path, asset: Vec<u8>) {
|
||||
let mut dir = self.clone();
|
||||
if let Some(parent) = path.parent() {
|
||||
dir = self.get_or_insert_dir(parent);
|
||||
}
|
||||
dir.0.write().assets.insert(
|
||||
path.file_name().unwrap().to_string_lossy().to_string(),
|
||||
Data(Arc::new((asset, path.to_owned()))),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn insert_meta(&self, path: &Path, asset: Vec<u8>) {
|
||||
let mut dir = self.clone();
|
||||
if let Some(parent) = path.parent() {
|
||||
dir = self.get_or_insert_dir(parent);
|
||||
}
|
||||
dir.0.write().metadata.insert(
|
||||
path.file_name().unwrap().to_string_lossy().to_string(),
|
||||
Data(Arc::new((asset, path.to_owned()))),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn get_or_insert_dir(&self, path: &Path) -> Dir {
|
||||
let mut dir = self.clone();
|
||||
let mut full_path = PathBuf::new();
|
||||
for c in path.components() {
|
||||
full_path.push(c);
|
||||
let name = c.as_os_str().to_string_lossy().to_string();
|
||||
dir = {
|
||||
let dirs = &mut dir.0.write().dirs;
|
||||
dirs.entry(name)
|
||||
.or_insert_with(|| Dir::new(full_path.clone()))
|
||||
.clone()
|
||||
};
|
||||
}
|
||||
|
||||
dir
|
||||
}
|
||||
|
||||
pub fn get_dir(&self, path: &Path) -> Option<Dir> {
|
||||
let mut dir = self.clone();
|
||||
for p in path.components() {
|
||||
let component = p.as_os_str().to_str().unwrap();
|
||||
let next_dir = dir.0.read().dirs.get(component)?.clone();
|
||||
dir = next_dir;
|
||||
}
|
||||
Some(dir)
|
||||
}
|
||||
|
||||
pub fn get_asset(&self, path: &Path) -> Option<Data> {
|
||||
let mut dir = self.clone();
|
||||
if let Some(parent) = path.parent() {
|
||||
dir = dir.get_dir(parent)?;
|
||||
}
|
||||
|
||||
path.file_name()
|
||||
.and_then(|f| dir.0.read().assets.get(f.to_str().unwrap()).cloned())
|
||||
}
|
||||
|
||||
pub fn get_metadata(&self, path: &Path) -> Option<Data> {
|
||||
let mut dir = self.clone();
|
||||
if let Some(parent) = path.parent() {
|
||||
dir = dir.get_dir(parent)?;
|
||||
}
|
||||
|
||||
path.file_name()
|
||||
.and_then(|f| dir.0.read().metadata.get(f.to_str().unwrap()).cloned())
|
||||
}
|
||||
|
||||
pub fn path(&self) -> PathBuf {
|
||||
self.0.read().path.to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DirStream {
|
||||
dir: Dir,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl DirStream {
|
||||
fn new(dir: Dir) -> Self {
|
||||
Self { dir, index: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for DirStream {
|
||||
type Item = PathBuf;
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
let index = this.index;
|
||||
this.index += 1;
|
||||
let dir = this.dir.0.read();
|
||||
Poll::Ready(dir.assets.values().nth(index).map(|d| d.path().to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
/// In-memory [`AssetReader`] implementation.
|
||||
/// This is primarily intended for unit tests.
|
||||
#[derive(Default, Clone)]
|
||||
pub struct MemoryAssetReader {
|
||||
pub root: Dir,
|
||||
}
|
||||
|
||||
/// Asset data stored in a [`Dir`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Data(Arc<(Vec<u8>, PathBuf)>);
|
||||
|
||||
impl Data {
|
||||
fn path(&self) -> &Path {
|
||||
&self.0 .1
|
||||
}
|
||||
fn data(&self) -> &[u8] {
|
||||
&self.0 .0
|
||||
}
|
||||
}
|
||||
|
||||
struct DataReader {
|
||||
data: Data,
|
||||
bytes_read: usize,
|
||||
}
|
||||
|
||||
impl AsyncRead for DataReader {
|
||||
fn poll_read(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> std::task::Poll<futures_io::Result<usize>> {
|
||||
if self.bytes_read >= self.data.data().len() {
|
||||
Poll::Ready(Ok(0))
|
||||
} else {
|
||||
let n = ready!(Pin::new(&mut &self.data.data()[self.bytes_read..]).poll_read(cx, buf))?;
|
||||
self.bytes_read += n;
|
||||
Poll::Ready(Ok(n))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetReader for MemoryAssetReader {
|
||||
fn read<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
self.root
|
||||
.get_asset(path)
|
||||
.map(|data| {
|
||||
let reader: Box<Reader> = Box::new(DataReader {
|
||||
data,
|
||||
bytes_read: 0,
|
||||
});
|
||||
reader
|
||||
})
|
||||
.ok_or(AssetReaderError::NotFound(PathBuf::new()))
|
||||
})
|
||||
}
|
||||
|
||||
fn read_meta<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
self.root
|
||||
.get_metadata(path)
|
||||
.map(|data| {
|
||||
let reader: Box<Reader> = Box::new(DataReader {
|
||||
data,
|
||||
bytes_read: 0,
|
||||
});
|
||||
reader
|
||||
})
|
||||
.ok_or(AssetReaderError::NotFound(PathBuf::new()))
|
||||
})
|
||||
}
|
||||
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
self.root
|
||||
.get_dir(path)
|
||||
.map(|dir| {
|
||||
let stream: Box<PathStream> = Box::new(DirStream::new(dir));
|
||||
stream
|
||||
})
|
||||
.ok_or(AssetReaderError::NotFound(PathBuf::new()))
|
||||
})
|
||||
}
|
||||
|
||||
fn is_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, std::result::Result<bool, AssetReaderError>> {
|
||||
Box::pin(async move { Ok(self.root.get_dir(path).is_some()) })
|
||||
}
|
||||
|
||||
fn watch_for_changes(
|
||||
&self,
|
||||
_event_sender: crossbeam_channel::Sender<super::AssetSourceEvent>,
|
||||
) -> Option<Box<dyn super::AssetWatcher>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use super::Dir;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn memory_dir() {
|
||||
let dir = Dir::default();
|
||||
let a_path = Path::new("a.txt");
|
||||
let a_data = "a".as_bytes().to_vec();
|
||||
let a_meta = "ameta".as_bytes().to_vec();
|
||||
|
||||
dir.insert_asset(a_path, a_data.clone());
|
||||
let asset = dir.get_asset(a_path).unwrap();
|
||||
assert_eq!(asset.path(), a_path);
|
||||
assert_eq!(asset.data(), a_data);
|
||||
|
||||
dir.insert_meta(a_path, a_meta.clone());
|
||||
let meta = dir.get_metadata(a_path).unwrap();
|
||||
assert_eq!(meta.path(), a_path);
|
||||
assert_eq!(meta.data(), a_meta);
|
||||
|
||||
let b_path = Path::new("x/y/b.txt");
|
||||
let b_data = "b".as_bytes().to_vec();
|
||||
let b_meta = "meta".as_bytes().to_vec();
|
||||
dir.insert_asset(b_path, b_data.clone());
|
||||
dir.insert_meta(b_path, b_meta.clone());
|
||||
|
||||
let asset = dir.get_asset(b_path).unwrap();
|
||||
assert_eq!(asset.path(), b_path);
|
||||
assert_eq!(asset.data(), b_data);
|
||||
|
||||
let meta = dir.get_metadata(b_path).unwrap();
|
||||
assert_eq!(meta.path(), b_path);
|
||||
assert_eq!(meta.data(), b_meta);
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
use std::convert::{TryFrom, TryInto};
|
||||
|
||||
/// A enum representing a type of file.
|
||||
#[non_exhaustive]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum FileType {
|
||||
/// A directory.
|
||||
Directory,
|
||||
/// A file.
|
||||
File,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
/// Returns `true` if the entry is a directory.
|
||||
#[inline]
|
||||
pub const fn is_dir(&self) -> bool {
|
||||
matches!(self, Self::Directory)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Returns `true` if the entry is a file.
|
||||
pub const fn is_file(&self) -> bool {
|
||||
matches!(self, Self::File)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<std::fs::FileType> for FileType {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn try_from(file_type: std::fs::FileType) -> Result<Self, Self::Error> {
|
||||
if file_type.is_dir() {
|
||||
Ok(Self::Directory)
|
||||
} else if file_type.is_file() {
|
||||
Ok(Self::File)
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"unknown file type",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata information about a file.
|
||||
///
|
||||
/// This structure is returned from the [`AssetIo::get_metadata`](crate::AssetIo) method.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Metadata {
|
||||
file_type: FileType,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
/// Creates new metadata information.
|
||||
pub fn new(file_type: FileType) -> Self {
|
||||
Self { file_type }
|
||||
}
|
||||
|
||||
/// Returns the file type.
|
||||
#[inline]
|
||||
pub const fn file_type(&self) -> FileType {
|
||||
self.file_type
|
||||
}
|
||||
|
||||
/// Returns `true` if the entry is a directory.
|
||||
#[inline]
|
||||
pub const fn is_dir(&self) -> bool {
|
||||
self.file_type.is_dir()
|
||||
}
|
||||
|
||||
/// Returns `true` if the entry is a file.
|
||||
#[inline]
|
||||
pub const fn is_file(&self) -> bool {
|
||||
self.file_type.is_file()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<std::fs::Metadata> for Metadata {
|
||||
type Error = std::io::Error;
|
||||
|
||||
fn try_from(metadata: std::fs::Metadata) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
file_type: metadata.file_type().try_into()?,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,105 +1,281 @@
|
|||
#[cfg(target_os = "android")]
|
||||
mod android_asset_io;
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
|
||||
mod file_asset_io;
|
||||
pub mod android;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod file;
|
||||
pub mod gated;
|
||||
pub mod memory;
|
||||
pub mod processor_gated;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod wasm_asset_io;
|
||||
pub mod wasm;
|
||||
|
||||
mod metadata;
|
||||
mod provider;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
pub use android_asset_io::*;
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
|
||||
pub use file_asset_io::*;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use wasm_asset_io::*;
|
||||
pub use futures_lite::{AsyncReadExt, AsyncWriteExt};
|
||||
pub use provider::*;
|
||||
|
||||
pub use metadata::*;
|
||||
|
||||
use anyhow::Result;
|
||||
use bevy_utils::BoxedFuture;
|
||||
use downcast_rs::{impl_downcast, Downcast};
|
||||
use crossbeam_channel::Sender;
|
||||
use futures_io::{AsyncRead, AsyncWrite};
|
||||
use futures_lite::{ready, Stream};
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
task::Poll,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::ChangeWatcher;
|
||||
|
||||
/// Errors that occur while loading assets.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AssetIoError {
|
||||
pub enum AssetReaderError {
|
||||
/// Path not found.
|
||||
#[error("path not found: {0}")]
|
||||
NotFound(PathBuf),
|
||||
|
||||
/// Encountered an I/O error while loading an asset.
|
||||
#[error("encountered an io error while loading asset: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
|
||||
/// Failed to watch path.
|
||||
#[error("failed to watch path: {0}")]
|
||||
PathWatchError(PathBuf),
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// A storage provider for an [`AssetServer`].
|
||||
pub type Reader<'a> = dyn AsyncRead + Unpin + Send + Sync + 'a;
|
||||
|
||||
/// Performs read operations on an asset storage. [`AssetReader`] exposes a "virtual filesystem"
|
||||
/// API, where asset bytes and asset metadata bytes are both stored and accessible for a given
|
||||
/// `path`.
|
||||
///
|
||||
/// An asset I/O is the backend actually providing data for the asset loaders managed by the asset
|
||||
/// server. An average user will probably be just fine with the default [`FileAssetIo`], but you
|
||||
/// can easily use your own custom I/O to, for example, load assets from cloud storage or create a
|
||||
/// seamless VFS layout using custom containers.
|
||||
///
|
||||
/// See the [`custom_asset_io`] example in the repository for more details.
|
||||
///
|
||||
/// [`AssetServer`]: struct.AssetServer.html
|
||||
/// [`custom_asset_io`]: https://github.com/bevyengine/bevy/tree/latest/examples/asset/custom_asset_io.rs
|
||||
pub trait AssetIo: Downcast + Send + Sync + 'static {
|
||||
/// Also see [`AssetWriter`].
|
||||
pub trait AssetReader: Send + Sync + 'static {
|
||||
/// Returns a future to load the full file data at the provided path.
|
||||
fn load_path<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<Vec<u8>, AssetIoError>>;
|
||||
|
||||
fn read<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>>;
|
||||
/// Returns a future to load the full file data at the provided path.
|
||||
fn read_meta<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>>;
|
||||
/// Returns an iterator of directory entry names at the provided path.
|
||||
fn read_directory(
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>>;
|
||||
/// Returns an iterator of directory entry names at the provided path.
|
||||
fn is_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<bool, AssetReaderError>>;
|
||||
|
||||
/// Returns an Asset watcher that will send events on the given channel.
|
||||
/// If this reader does not support watching for changes, this will return [`None`].
|
||||
fn watch_for_changes(
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> Result<Box<dyn Iterator<Item = PathBuf>>, AssetIoError>;
|
||||
event_sender: Sender<AssetSourceEvent>,
|
||||
) -> Option<Box<dyn AssetWatcher>>;
|
||||
|
||||
/// Returns metadata about the filesystem entry at the provided path.
|
||||
fn get_metadata(&self, path: &Path) -> Result<Metadata, AssetIoError>;
|
||||
|
||||
/// Tells the asset I/O to watch for changes recursively at the provided path.
|
||||
///
|
||||
/// No-op if [`watch_for_changes`](AssetIo::watch_for_changes) hasn't been called yet.
|
||||
/// Otherwise triggers a reload each time `to_watch` changes.
|
||||
/// In most cases the asset found at the watched path should be changed,
|
||||
/// but when an asset depends on data at another path, the asset's path
|
||||
/// is provided in `to_reload`.
|
||||
/// Note that there may be a many-to-many correspondence between
|
||||
/// `to_watch` and `to_reload` paths.
|
||||
fn watch_path_for_changes(
|
||||
&self,
|
||||
to_watch: &Path,
|
||||
to_reload: Option<PathBuf>,
|
||||
) -> Result<(), AssetIoError>;
|
||||
|
||||
/// Enables change tracking in this asset I/O.
|
||||
fn watch_for_changes(&self, configuration: &ChangeWatcher) -> Result<(), AssetIoError>;
|
||||
|
||||
/// Returns `true` if the path is a directory.
|
||||
fn is_dir(&self, path: &Path) -> bool {
|
||||
self.get_metadata(path)
|
||||
.as_ref()
|
||||
.map(Metadata::is_dir)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Returns `true` if the path is a file.
|
||||
fn is_file(&self, path: &Path) -> bool {
|
||||
self.get_metadata(path)
|
||||
.as_ref()
|
||||
.map(Metadata::is_file)
|
||||
.unwrap_or(false)
|
||||
/// Reads asset metadata bytes at the given `path` into a [`Vec<u8>`]. This is a convenience
|
||||
/// function that wraps [`AssetReader::read_meta`] by default.
|
||||
fn read_meta_bytes<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Vec<u8>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
let mut meta_reader = self.read_meta(path).await?;
|
||||
let mut meta_bytes = Vec::new();
|
||||
meta_reader.read_to_end(&mut meta_bytes).await?;
|
||||
Ok(meta_bytes)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl_downcast!(AssetIo);
|
||||
pub type Writer = dyn AsyncWrite + Unpin + Send + Sync;
|
||||
|
||||
pub type PathStream = dyn Stream<Item = PathBuf> + Unpin + Send;
|
||||
|
||||
/// Errors that occur while loading assets.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AssetWriterError {
|
||||
/// Encountered an I/O error while loading an asset.
|
||||
#[error("encountered an io error while loading asset: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// Preforms write operations on an asset storage. [`AssetWriter`] exposes a "virtual filesystem"
|
||||
/// API, where asset bytes and asset metadata bytes are both stored and accessible for a given
|
||||
/// `path`.
|
||||
///
|
||||
/// Also see [`AssetReader`].
|
||||
pub trait AssetWriter: Send + Sync + 'static {
|
||||
/// Writes the full asset bytes at the provided path.
|
||||
fn write<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>>;
|
||||
/// Writes the full asset meta bytes at the provided path.
|
||||
/// This _should not_ include storage specific extensions like `.meta`.
|
||||
fn write_meta<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Writer>, AssetWriterError>>;
|
||||
/// Removes the asset stored at the given path.
|
||||
fn remove<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
|
||||
/// Removes the asset meta stored at the given path.
|
||||
/// This _should not_ include storage specific extensions like `.meta`.
|
||||
fn remove_meta<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
|
||||
/// Renames the asset at `old_path` to `new_path`
|
||||
fn rename<'a>(
|
||||
&'a self,
|
||||
old_path: &'a Path,
|
||||
new_path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
|
||||
/// Renames the asset meta for the asset at `old_path` to `new_path`.
|
||||
/// This _should not_ include storage specific extensions like `.meta`.
|
||||
fn rename_meta<'a>(
|
||||
&'a self,
|
||||
old_path: &'a Path,
|
||||
new_path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
|
||||
/// Removes the directory at the given path, including all assets _and_ directories in that directory.
|
||||
fn remove_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
|
||||
/// Removes the directory at the given path, but only if it is completely empty. This will return an error if the
|
||||
/// directory is not empty.
|
||||
fn remove_empty_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
|
||||
/// Removes all assets (and directories) in this directory, resulting in an empty directory.
|
||||
fn remove_assets_in_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<(), AssetWriterError>>;
|
||||
/// Writes the asset `bytes` to the given `path`.
|
||||
fn write_bytes<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
bytes: &'a [u8],
|
||||
) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
|
||||
Box::pin(async move {
|
||||
let mut writer = self.write(path).await?;
|
||||
writer.write_all(bytes).await?;
|
||||
writer.flush().await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
/// Writes the asset meta `bytes` to the given `path`.
|
||||
fn write_meta_bytes<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
bytes: &'a [u8],
|
||||
) -> BoxedFuture<'a, Result<(), AssetWriterError>> {
|
||||
Box::pin(async move {
|
||||
let mut meta_writer = self.write_meta(path).await?;
|
||||
meta_writer.write_all(bytes).await?;
|
||||
meta_writer.flush().await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An "asset source change event" that occurs whenever asset (or asset metadata) is created/added/removed
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AssetSourceEvent {
|
||||
/// An asset at this path was added.
|
||||
AddedAsset(PathBuf),
|
||||
/// An asset at this path was modified.
|
||||
ModifiedAsset(PathBuf),
|
||||
/// An asset at this path was removed.
|
||||
RemovedAsset(PathBuf),
|
||||
/// An asset at this path was renamed.
|
||||
RenamedAsset { old: PathBuf, new: PathBuf },
|
||||
/// Asset metadata at this path was added.
|
||||
AddedMeta(PathBuf),
|
||||
/// Asset metadata at this path was modified.
|
||||
ModifiedMeta(PathBuf),
|
||||
/// Asset metadata at this path was removed.
|
||||
RemovedMeta(PathBuf),
|
||||
/// Asset metadata at this path was renamed.
|
||||
RenamedMeta { old: PathBuf, new: PathBuf },
|
||||
/// A folder at the given path was added.
|
||||
AddedFolder(PathBuf),
|
||||
/// A folder at the given path was removed.
|
||||
RemovedFolder(PathBuf),
|
||||
/// A folder at the given path was renamed.
|
||||
RenamedFolder { old: PathBuf, new: PathBuf },
|
||||
/// Something of unknown type was removed. It is the job of the event handler to determine the type.
|
||||
/// This exists because notify-rs produces "untyped" rename events without destination paths for unwatched folders, so we can't determine the type of
|
||||
/// the rename.
|
||||
RemovedUnknown {
|
||||
/// The path of the removed asset or folder (undetermined). This could be an asset path or a folder. This will not be a "meta file" path.
|
||||
path: PathBuf,
|
||||
/// This field is only relevant if `path` is determined to be an asset path (and therefore not a folder). If this field is `true`,
|
||||
/// then this event corresponds to a meta removal (not an asset removal) . If `false`, then this event corresponds to an asset removal
|
||||
/// (not a meta removal).
|
||||
is_meta: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// A handle to an "asset watcher" process, that will listen for and emit [`AssetSourceEvent`] values for as long as
|
||||
/// [`AssetWatcher`] has not been dropped.
|
||||
///
|
||||
/// See [`AssetReader::watch_for_changes`].
|
||||
pub trait AssetWatcher: Send + Sync + 'static {}
|
||||
|
||||
/// An [`AsyncRead`] implementation capable of reading a [`Vec<u8>`].
|
||||
pub struct VecReader {
|
||||
bytes: Vec<u8>,
|
||||
bytes_read: usize,
|
||||
}
|
||||
|
||||
impl VecReader {
|
||||
/// Create a new [`VecReader`] for `bytes`.
|
||||
pub fn new(bytes: Vec<u8>) -> Self {
|
||||
Self {
|
||||
bytes_read: 0,
|
||||
bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for VecReader {
|
||||
fn poll_read(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> std::task::Poll<futures_io::Result<usize>> {
|
||||
if self.bytes_read >= self.bytes.len() {
|
||||
Poll::Ready(Ok(0))
|
||||
} else {
|
||||
let n = ready!(Pin::new(&mut &self.bytes[self.bytes_read..]).poll_read(cx, buf))?;
|
||||
self.bytes_read += n;
|
||||
Poll::Ready(Ok(n))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends `.meta` to the given path.
|
||||
pub(crate) fn get_meta_path(path: &Path) -> PathBuf {
|
||||
let mut meta_path = path.to_path_buf();
|
||||
let mut extension = path
|
||||
.extension()
|
||||
.expect("asset paths must have extensions")
|
||||
.to_os_string();
|
||||
extension.push(".meta");
|
||||
meta_path.set_extension(extension);
|
||||
meta_path
|
||||
}
|
||||
|
||||
/// A [`PathBuf`] [`Stream`] implementation that immediately returns nothing.
|
||||
struct EmptyPathStream;
|
||||
|
||||
impl Stream for EmptyPathStream {
|
||||
type Item = PathBuf;
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut std::task::Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
Poll::Ready(None)
|
||||
}
|
||||
}
|
||||
|
|
163
crates/bevy_asset/src/io/processor_gated.rs
Normal file
163
crates/bevy_asset/src/io/processor_gated.rs
Normal file
|
@ -0,0 +1,163 @@
|
|||
use crate::{
|
||||
io::{AssetReader, AssetReaderError, PathStream, Reader},
|
||||
processor::{AssetProcessorData, ProcessStatus},
|
||||
AssetPath,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_lock::RwLockReadGuardArc;
|
||||
use bevy_log::trace;
|
||||
use bevy_utils::BoxedFuture;
|
||||
use futures_io::AsyncRead;
|
||||
use std::{path::Path, pin::Pin, sync::Arc};
|
||||
|
||||
/// An [`AssetReader`] that will prevent asset (and asset metadata) read futures from returning for a
|
||||
/// given path until that path has been processed by [`AssetProcessor`].
|
||||
///
|
||||
/// [`AssetProcessor`]: crate::processor::AssetProcessor
|
||||
pub struct ProcessorGatedReader {
|
||||
reader: Box<dyn AssetReader>,
|
||||
processor_data: Arc<AssetProcessorData>,
|
||||
}
|
||||
|
||||
impl ProcessorGatedReader {
|
||||
/// Creates a new [`ProcessorGatedReader`].
|
||||
pub fn new(reader: Box<dyn AssetReader>, processor_data: Arc<AssetProcessorData>) -> Self {
|
||||
Self {
|
||||
processor_data,
|
||||
reader,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a "transaction lock" that can be used to ensure no writes to asset or asset meta occur
|
||||
/// while it is held.
|
||||
async fn get_transaction_lock(
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> Result<RwLockReadGuardArc<()>, AssetReaderError> {
|
||||
let infos = self.processor_data.asset_infos.read().await;
|
||||
let info = infos
|
||||
.get(&AssetPath::new(path.to_owned(), None))
|
||||
.ok_or_else(|| AssetReaderError::NotFound(path.to_owned()))?;
|
||||
Ok(info.file_transaction_lock.read_arc().await)
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetReader for ProcessorGatedReader {
|
||||
fn read<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
trace!("Waiting for processing to finish before reading {:?}", path);
|
||||
let process_result = self.processor_data.wait_until_processed(path).await;
|
||||
match process_result {
|
||||
ProcessStatus::Processed => {}
|
||||
ProcessStatus::Failed | ProcessStatus::NonExistent => {
|
||||
return Err(AssetReaderError::NotFound(path.to_owned()))
|
||||
}
|
||||
}
|
||||
trace!(
|
||||
"Processing finished with {:?}, reading {:?}",
|
||||
process_result,
|
||||
path
|
||||
);
|
||||
let lock = self.get_transaction_lock(path).await?;
|
||||
let asset_reader = self.reader.read(path).await?;
|
||||
let reader: Box<Reader<'a>> =
|
||||
Box::new(TransactionLockedReader::new(asset_reader, lock));
|
||||
Ok(reader)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_meta<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
trace!(
|
||||
"Waiting for processing to finish before reading meta {:?}",
|
||||
path
|
||||
);
|
||||
let process_result = self.processor_data.wait_until_processed(path).await;
|
||||
match process_result {
|
||||
ProcessStatus::Processed => {}
|
||||
ProcessStatus::Failed | ProcessStatus::NonExistent => {
|
||||
return Err(AssetReaderError::NotFound(path.to_owned()));
|
||||
}
|
||||
}
|
||||
trace!(
|
||||
"Processing finished with {:?}, reading meta {:?}",
|
||||
process_result,
|
||||
path
|
||||
);
|
||||
let lock = self.get_transaction_lock(path).await?;
|
||||
let meta_reader = self.reader.read_meta(path).await?;
|
||||
let reader: Box<Reader<'a>> = Box::new(TransactionLockedReader::new(meta_reader, lock));
|
||||
Ok(reader)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
trace!(
|
||||
"Waiting for processing to finish before reading directory {:?}",
|
||||
path
|
||||
);
|
||||
self.processor_data.wait_until_finished().await;
|
||||
trace!("Processing finished, reading directory {:?}", path);
|
||||
let result = self.reader.read_directory(path).await?;
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_directory<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, std::result::Result<bool, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
trace!(
|
||||
"Waiting for processing to finish before reading directory {:?}",
|
||||
path
|
||||
);
|
||||
self.processor_data.wait_until_finished().await;
|
||||
trace!("Processing finished, getting directory status {:?}", path);
|
||||
let result = self.reader.is_directory(path).await?;
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
|
||||
fn watch_for_changes(
|
||||
&self,
|
||||
event_sender: crossbeam_channel::Sender<super::AssetSourceEvent>,
|
||||
) -> Option<Box<dyn super::AssetWatcher>> {
|
||||
self.reader.watch_for_changes(event_sender)
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`AsyncRead`] impl that will hold its asset's transaction lock until [`TransactionLockedReader`] is dropped.
|
||||
pub struct TransactionLockedReader<'a> {
|
||||
reader: Box<Reader<'a>>,
|
||||
_file_transaction_lock: RwLockReadGuardArc<()>,
|
||||
}
|
||||
|
||||
impl<'a> TransactionLockedReader<'a> {
|
||||
fn new(reader: Box<Reader<'a>>, file_transaction_lock: RwLockReadGuardArc<()>) -> Self {
|
||||
Self {
|
||||
reader,
|
||||
_file_transaction_lock: file_transaction_lock,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsyncRead for TransactionLockedReader<'a> {
|
||||
fn poll_read(
|
||||
mut self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> std::task::Poll<futures_io::Result<usize>> {
|
||||
Pin::new(&mut self.reader).poll_read(cx, buf)
|
||||
}
|
||||
}
|
190
crates/bevy_asset/src/io/provider.rs
Normal file
190
crates/bevy_asset/src/io/provider.rs
Normal file
|
@ -0,0 +1,190 @@
|
|||
use bevy_ecs::system::Resource;
|
||||
use bevy_utils::HashMap;
|
||||
|
||||
use crate::{
|
||||
io::{AssetReader, AssetWriter},
|
||||
AssetPlugin,
|
||||
};
|
||||
|
||||
/// A reference to an "asset provider", which maps to an [`AssetReader`] and/or [`AssetWriter`].
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub enum AssetProvider {
|
||||
/// The default asset provider
|
||||
#[default]
|
||||
Default,
|
||||
/// A custom / named asset provider
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
/// A [`Resource`] that hold (repeatable) functions capable of producing new [`AssetReader`] and [`AssetWriter`] instances
|
||||
/// for a given [`AssetProvider`].
|
||||
#[derive(Resource, Default)]
|
||||
pub struct AssetProviders {
|
||||
readers: HashMap<String, Box<dyn FnMut() -> Box<dyn AssetReader> + Send + Sync>>,
|
||||
writers: HashMap<String, Box<dyn FnMut() -> Box<dyn AssetWriter> + Send + Sync>>,
|
||||
default_file_source: Option<String>,
|
||||
default_file_destination: Option<String>,
|
||||
}
|
||||
|
||||
impl AssetProviders {
|
||||
/// Inserts a new `get_reader` function with the given `provider` name. This function will be used to create new [`AssetReader`]s
|
||||
/// when they are requested for the given `provider`.
|
||||
pub fn insert_reader(
|
||||
&mut self,
|
||||
provider: &str,
|
||||
get_reader: impl FnMut() -> Box<dyn AssetReader> + Send + Sync + 'static,
|
||||
) {
|
||||
self.readers
|
||||
.insert(provider.to_string(), Box::new(get_reader));
|
||||
}
|
||||
/// Inserts a new `get_reader` function with the given `provider` name. This function will be used to create new [`AssetReader`]s
|
||||
/// when they are requested for the given `provider`.
|
||||
pub fn with_reader(
|
||||
mut self,
|
||||
provider: &str,
|
||||
get_reader: impl FnMut() -> Box<dyn AssetReader> + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
self.insert_reader(provider, get_reader);
|
||||
self
|
||||
}
|
||||
/// Inserts a new `get_writer` function with the given `provider` name. This function will be used to create new [`AssetWriter`]s
|
||||
/// when they are requested for the given `provider`.
|
||||
pub fn insert_writer(
|
||||
&mut self,
|
||||
provider: &str,
|
||||
get_writer: impl FnMut() -> Box<dyn AssetWriter> + Send + Sync + 'static,
|
||||
) {
|
||||
self.writers
|
||||
.insert(provider.to_string(), Box::new(get_writer));
|
||||
}
|
||||
/// Inserts a new `get_writer` function with the given `provider` name. This function will be used to create new [`AssetWriter`]s
|
||||
/// when they are requested for the given `provider`.
|
||||
pub fn with_writer(
|
||||
mut self,
|
||||
provider: &str,
|
||||
get_writer: impl FnMut() -> Box<dyn AssetWriter> + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
self.insert_writer(provider, get_writer);
|
||||
self
|
||||
}
|
||||
/// Returns the default "asset source" path for the [`FileAssetReader`] and [`FileAssetWriter`].
|
||||
///
|
||||
/// [`FileAssetReader`]: crate::io::file::FileAssetReader
|
||||
/// [`FileAssetWriter`]: crate::io::file::FileAssetWriter
|
||||
pub fn default_file_source(&self) -> &str {
|
||||
self.default_file_source
|
||||
.as_deref()
|
||||
.unwrap_or(AssetPlugin::DEFAULT_FILE_SOURCE)
|
||||
}
|
||||
|
||||
/// Sets the default "asset source" path for the [`FileAssetReader`] and [`FileAssetWriter`].
|
||||
///
|
||||
/// [`FileAssetReader`]: crate::io::file::FileAssetReader
|
||||
/// [`FileAssetWriter`]: crate::io::file::FileAssetWriter
|
||||
pub fn with_default_file_source(mut self, path: String) -> Self {
|
||||
self.default_file_source = Some(path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the default "asset destination" path for the [`FileAssetReader`] and [`FileAssetWriter`].
|
||||
///
|
||||
/// [`FileAssetReader`]: crate::io::file::FileAssetReader
|
||||
/// [`FileAssetWriter`]: crate::io::file::FileAssetWriter
|
||||
pub fn with_default_file_destination(mut self, path: String) -> Self {
|
||||
self.default_file_destination = Some(path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the default "asset destination" path for the [`FileAssetReader`] and [`FileAssetWriter`].
|
||||
///
|
||||
/// [`FileAssetReader`]: crate::io::file::FileAssetReader
|
||||
/// [`FileAssetWriter`]: crate::io::file::FileAssetWriter
|
||||
pub fn default_file_destination(&self) -> &str {
|
||||
self.default_file_destination
|
||||
.as_deref()
|
||||
.unwrap_or(AssetPlugin::DEFAULT_FILE_DESTINATION)
|
||||
}
|
||||
|
||||
/// Returns a new "source" [`AssetReader`] for the given [`AssetProvider`].
|
||||
pub fn get_source_reader(&mut self, provider: &AssetProvider) -> Box<dyn AssetReader> {
|
||||
match provider {
|
||||
AssetProvider::Default => {
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
|
||||
let reader = super::file::FileAssetReader::new(self.default_file_source());
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let reader = super::wasm::HttpWasmAssetReader::new(self.default_file_source());
|
||||
#[cfg(target_os = "android")]
|
||||
let reader = super::android::AndroidAssetReader;
|
||||
Box::new(reader)
|
||||
}
|
||||
AssetProvider::Custom(provider) => {
|
||||
let get_reader = self
|
||||
.readers
|
||||
.get_mut(provider)
|
||||
.unwrap_or_else(|| panic!("Asset Provider {} does not exist", provider));
|
||||
(get_reader)()
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Returns a new "destination" [`AssetReader`] for the given [`AssetProvider`].
|
||||
pub fn get_destination_reader(&mut self, provider: &AssetProvider) -> Box<dyn AssetReader> {
|
||||
match provider {
|
||||
AssetProvider::Default => {
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
|
||||
let reader = super::file::FileAssetReader::new(self.default_file_destination());
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let reader = super::wasm::HttpWasmAssetReader::new(self.default_file_destination());
|
||||
#[cfg(target_os = "android")]
|
||||
let reader = super::android::AndroidAssetReader;
|
||||
Box::new(reader)
|
||||
}
|
||||
AssetProvider::Custom(provider) => {
|
||||
let get_reader = self
|
||||
.readers
|
||||
.get_mut(provider)
|
||||
.unwrap_or_else(|| panic!("Asset Provider {} does not exist", provider));
|
||||
(get_reader)()
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Returns a new "source" [`AssetWriter`] for the given [`AssetProvider`].
|
||||
pub fn get_source_writer(&mut self, provider: &AssetProvider) -> Box<dyn AssetWriter> {
|
||||
match provider {
|
||||
AssetProvider::Default => {
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
|
||||
return Box::new(super::file::FileAssetWriter::new(
|
||||
self.default_file_source(),
|
||||
));
|
||||
#[cfg(any(target_arch = "wasm32", target_os = "android"))]
|
||||
panic!("Writing assets isn't supported on this platform yet");
|
||||
}
|
||||
AssetProvider::Custom(provider) => {
|
||||
let get_writer = self
|
||||
.writers
|
||||
.get_mut(provider)
|
||||
.unwrap_or_else(|| panic!("Asset Provider {} does not exist", provider));
|
||||
(get_writer)()
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Returns a new "destination" [`AssetWriter`] for the given [`AssetProvider`].
|
||||
pub fn get_destination_writer(&mut self, provider: &AssetProvider) -> Box<dyn AssetWriter> {
|
||||
match provider {
|
||||
AssetProvider::Default => {
|
||||
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
|
||||
return Box::new(super::file::FileAssetWriter::new(
|
||||
self.default_file_destination(),
|
||||
));
|
||||
#[cfg(any(target_arch = "wasm32", target_os = "android"))]
|
||||
panic!("Writing assets isn't supported on this platform yet");
|
||||
}
|
||||
AssetProvider::Custom(provider) => {
|
||||
let get_writer = self
|
||||
.writers
|
||||
.get_mut(provider)
|
||||
.unwrap_or_else(|| panic!("Asset Provider {} does not exist", provider));
|
||||
(get_writer)()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
110
crates/bevy_asset/src/io/wasm.rs
Normal file
110
crates/bevy_asset/src/io/wasm.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
use crate::io::{
|
||||
get_meta_path, AssetReader, AssetReaderError, AssetWatcher, EmptyPathStream, PathStream,
|
||||
Reader, VecReader,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use bevy_log::error;
|
||||
use bevy_utils::BoxedFuture;
|
||||
use js_sys::{Uint8Array, JSON};
|
||||
use std::path::{Path, PathBuf};
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::Response;
|
||||
|
||||
/// Reader implementation for loading assets via HTTP in WASM.
|
||||
pub struct HttpWasmAssetReader {
|
||||
root_path: PathBuf,
|
||||
}
|
||||
|
||||
impl HttpWasmAssetReader {
|
||||
/// Creates a new `WasmAssetReader`. The path provided will be used to build URLs to query for assets.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
||||
Self {
|
||||
root_path: path.as_ref().to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn js_value_to_err<'a>(context: &'a str) -> impl FnOnce(JsValue) -> std::io::Error + 'a {
|
||||
move |value| {
|
||||
let message = match JSON::stringify(&value) {
|
||||
Ok(js_str) => format!("Failed to {context}: {js_str}"),
|
||||
Err(_) => {
|
||||
format!("Failed to {context} and also failed to stringify the JSValue of the error")
|
||||
}
|
||||
};
|
||||
|
||||
std::io::Error::new(std::io::ErrorKind::Other, message)
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpWasmAssetReader {
|
||||
async fn fetch_bytes<'a>(&self, path: PathBuf) -> Result<Box<Reader<'a>>, AssetReaderError> {
|
||||
let window = web_sys::window().unwrap();
|
||||
let resp_value = JsFuture::from(window.fetch_with_str(path.to_str().unwrap()))
|
||||
.await
|
||||
.map_err(js_value_to_err("fetch path"))?;
|
||||
let resp = resp_value
|
||||
.dyn_into::<Response>()
|
||||
.map_err(js_value_to_err("convert fetch to Response"))?;
|
||||
match resp.status() {
|
||||
200 => {
|
||||
let data = JsFuture::from(resp.array_buffer().unwrap()).await.unwrap();
|
||||
let bytes = Uint8Array::new(&data).to_vec();
|
||||
let reader: Box<Reader> = Box::new(VecReader::new(bytes));
|
||||
Ok(reader)
|
||||
}
|
||||
404 => Err(AssetReaderError::NotFound(path)),
|
||||
status => Err(AssetReaderError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Encountered unexpected HTTP status {status}"),
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetReader for HttpWasmAssetReader {
|
||||
fn read<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
let path = self.root_path.join(path);
|
||||
self.fetch_bytes(path).await
|
||||
})
|
||||
}
|
||||
|
||||
fn read_meta<'a>(
|
||||
&'a self,
|
||||
path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
|
||||
Box::pin(async move {
|
||||
let meta_path = get_meta_path(path);
|
||||
Ok(self.fetch_bytes(meta_path).await?)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
_path: &'a Path,
|
||||
) -> BoxedFuture<'a, Result<Box<PathStream>, AssetReaderError>> {
|
||||
let stream: Box<PathStream> = Box::new(EmptyPathStream);
|
||||
error!("Reading directories is not supported with the HttpWasmAssetReader");
|
||||
Box::pin(async move { Ok(stream) })
|
||||
}
|
||||
|
||||
fn is_directory<'a>(
|
||||
&'a self,
|
||||
_path: &'a Path,
|
||||
) -> BoxedFuture<'a, std::result::Result<bool, AssetReaderError>> {
|
||||
error!("Reading directories is not supported with the HttpWasmAssetReader");
|
||||
Box::pin(async move { Ok(false) })
|
||||
}
|
||||
|
||||
fn watch_for_changes(
|
||||
&self,
|
||||
_event_sender: crossbeam_channel::Sender<super::AssetSourceEvent>,
|
||||
) -> Option<Box<dyn AssetWatcher>> {
|
||||
None
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
use crate::{AssetIo, AssetIoError, ChangeWatcher, Metadata};
|
||||
use anyhow::Result;
|
||||
use bevy_utils::BoxedFuture;
|
||||
use js_sys::Uint8Array;
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::Response;
|
||||
|
||||
/// I/O implementation for web builds.
|
||||
///
|
||||
/// Implementation details:
|
||||
///
|
||||
/// - `load_path` makes [fetch()] requests.
|
||||
/// - `read_directory` always returns an empty iterator.
|
||||
/// - `get_metadata` will always return an error.
|
||||
/// - Watching for changes is not supported. The watcher methods will do nothing.
|
||||
///
|
||||
/// [fetch()]: https://developer.mozilla.org/en-US/docs/Web/API/fetch
|
||||
pub struct WasmAssetIo {
|
||||
root_path: PathBuf,
|
||||
}
|
||||
|
||||
impl WasmAssetIo {
|
||||
/// Creates a new `WasmAssetIo`. The path provided will be used to build URLs to query for assets.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
||||
WasmAssetIo {
|
||||
root_path: path.as_ref().to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetIo for WasmAssetIo {
|
||||
fn load_path<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<Vec<u8>, AssetIoError>> {
|
||||
Box::pin(async move {
|
||||
let path = self.root_path.join(path);
|
||||
let window = web_sys::window().unwrap();
|
||||
let resp_value = JsFuture::from(window.fetch_with_str(path.to_str().unwrap()))
|
||||
.await
|
||||
.unwrap();
|
||||
let resp: Response = resp_value.dyn_into().unwrap();
|
||||
let data = JsFuture::from(resp.array_buffer().unwrap()).await.unwrap();
|
||||
let bytes = Uint8Array::new(&data).to_vec();
|
||||
Ok(bytes)
|
||||
})
|
||||
}
|
||||
|
||||
fn read_directory(
|
||||
&self,
|
||||
_path: &Path,
|
||||
) -> Result<Box<dyn Iterator<Item = PathBuf>>, AssetIoError> {
|
||||
bevy_log::warn!("Loading folders is not supported in WASM");
|
||||
Ok(Box::new(std::iter::empty::<PathBuf>()))
|
||||
}
|
||||
|
||||
fn watch_path_for_changes(
|
||||
&self,
|
||||
_to_watch: &Path,
|
||||
_to_reload: Option<PathBuf>,
|
||||
) -> Result<(), AssetIoError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn watch_for_changes(&self, _configuration: &ChangeWatcher) -> Result<(), AssetIoError> {
|
||||
bevy_log::warn!("Watching for changes is not supported in WASM");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_metadata(&self, path: &Path) -> Result<Metadata, AssetIoError> {
|
||||
let full_path = self.root_path.join(path);
|
||||
full_path
|
||||
.metadata()
|
||||
.and_then(Metadata::try_from)
|
||||
.map_err(|e| {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
AssetIoError::NotFound(full_path)
|
||||
} else {
|
||||
e.into()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,282 +1,547 @@
|
|||
use crate::{
|
||||
path::AssetPath, AssetIo, AssetIoError, AssetMeta, AssetServer, Assets, Handle, HandleId,
|
||||
HandleUntyped, RefChangeChannel,
|
||||
io::{AssetReaderError, Reader},
|
||||
meta::{
|
||||
loader_settings_meta_transform, AssetHash, AssetMeta, AssetMetaDyn, ProcessedInfoMinimal,
|
||||
Settings,
|
||||
},
|
||||
path::AssetPath,
|
||||
Asset, AssetLoadError, AssetServer, Assets, Handle, UntypedAssetId, UntypedHandle,
|
||||
};
|
||||
use anyhow::Error;
|
||||
use anyhow::Result;
|
||||
use bevy_ecs::system::{Res, ResMut};
|
||||
use bevy_reflect::TypePath;
|
||||
use bevy_reflect::{TypeUuid, TypeUuidDynamic};
|
||||
use bevy_utils::{BoxedFuture, HashMap};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use bevy_ecs::world::World;
|
||||
use bevy_utils::{BoxedFuture, HashMap, HashSet};
|
||||
use downcast_rs::{impl_downcast, Downcast};
|
||||
use std::path::Path;
|
||||
use futures_lite::AsyncReadExt;
|
||||
use ron::error::SpannedError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
path::Path,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
/// A loader for an asset source.
|
||||
///
|
||||
/// Types implementing this trait are used by the [`AssetServer`] to load assets
|
||||
/// into their respective asset storages.
|
||||
/// Loads an [`Asset`] from a given byte [`Reader`]. This can accept [`AssetLoader::Settings`], which configure how the [`Asset`]
|
||||
/// should be loaded.
|
||||
pub trait AssetLoader: Send + Sync + 'static {
|
||||
/// Processes the asset in an asynchronous closure.
|
||||
/// The top level [`Asset`] loaded by this [`AssetLoader`].
|
||||
type Asset: crate::Asset;
|
||||
/// The settings type used by this [`AssetLoader`].
|
||||
type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
|
||||
/// Asynchronously loads [`AssetLoader::Asset`] (and any other labeled assets) from the bytes provided by [`Reader`].
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
bytes: &'a [u8],
|
||||
reader: &'a mut Reader,
|
||||
settings: &'a Self::Settings,
|
||||
load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, Result<(), Error>>;
|
||||
) -> BoxedFuture<'a, Result<Self::Asset, anyhow::Error>>;
|
||||
|
||||
/// Returns a list of extensions supported by this asset loader, without the preceding dot.
|
||||
fn extensions(&self) -> &[&str];
|
||||
}
|
||||
|
||||
/// An essential piece of data of an application.
|
||||
///
|
||||
/// Assets are the building blocks of games. They can be anything, from images and sounds to scenes
|
||||
/// and scripts. In Bevy, an asset is any struct that has an unique type id, as shown below:
|
||||
///
|
||||
/// ```rust
|
||||
/// use bevy_reflect::{TypePath, TypeUuid};
|
||||
/// use serde::Deserialize;
|
||||
///
|
||||
/// #[derive(Debug, Deserialize, TypeUuid, TypePath)]
|
||||
/// #[uuid = "39cadc56-aa9c-4543-8640-a018b74b5052"]
|
||||
/// pub struct CustomAsset {
|
||||
/// pub value: i32,
|
||||
/// }
|
||||
/// # fn is_asset<T: bevy_asset::Asset>() {}
|
||||
/// # is_asset::<CustomAsset>();
|
||||
/// ```
|
||||
///
|
||||
/// See the `assets/custom_asset.rs` example in the repository for more details.
|
||||
///
|
||||
/// In order to load assets into your game you must either add them manually to an asset storage
|
||||
/// with [`Assets::add`] or load them from the filesystem with [`AssetServer::load`].
|
||||
pub trait Asset: TypeUuid + TypePath + AssetDynamic {}
|
||||
/// Provides type-erased access to an [`AssetLoader`].
|
||||
pub trait ErasedAssetLoader: Send + Sync + 'static {
|
||||
/// Asynchronously loads the asset(s) from the bytes provided by [`Reader`].
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
reader: &'a mut Reader,
|
||||
meta: Box<dyn AssetMetaDyn>,
|
||||
load_context: LoadContext<'a>,
|
||||
) -> BoxedFuture<'a, Result<ErasedLoadedAsset, AssetLoaderError>>;
|
||||
|
||||
/// An untyped version of the [`Asset`] trait.
|
||||
pub trait AssetDynamic: Downcast + TypeUuidDynamic + Send + Sync + 'static {}
|
||||
impl_downcast!(AssetDynamic);
|
||||
|
||||
impl<T> Asset for T where T: TypeUuid + TypePath + AssetDynamic + TypeUuidDynamic {}
|
||||
|
||||
impl<T> AssetDynamic for T where T: Send + Sync + 'static + TypeUuidDynamic {}
|
||||
|
||||
/// A complete asset processed in an [`AssetLoader`].
|
||||
pub struct LoadedAsset<T: Asset> {
|
||||
pub(crate) value: Option<T>,
|
||||
pub(crate) dependencies: Vec<AssetPath<'static>>,
|
||||
/// Returns a list of extensions supported by this asset loader, without the preceding dot.
|
||||
fn extensions(&self) -> &[&str];
|
||||
/// Deserializes metadata from the input `meta` bytes into the appropriate type (erased as [`Box<dyn AssetMetaDyn>`]).
|
||||
fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError>;
|
||||
/// Returns the default meta value for the [`AssetLoader`] (erased as [`Box<dyn AssetMetaDyn>`]).
|
||||
fn default_meta(&self) -> Box<dyn AssetMetaDyn>;
|
||||
/// Returns the type name of the [`AssetLoader`].
|
||||
fn type_name(&self) -> &'static str;
|
||||
/// Returns the [`TypeId`] of the [`AssetLoader`].
|
||||
fn type_id(&self) -> TypeId;
|
||||
/// Returns the type name of the top-level [`Asset`] loaded by the [`AssetLoader`].
|
||||
fn asset_type_name(&self) -> &'static str;
|
||||
/// Returns the [`TypeId`] of the top-level [`Asset`] loaded by the [`AssetLoader`].
|
||||
fn asset_type_id(&self) -> TypeId;
|
||||
}
|
||||
|
||||
impl<T: Asset> LoadedAsset<T> {
|
||||
/// Creates a new loaded asset.
|
||||
pub fn new(value: T) -> Self {
|
||||
Self {
|
||||
value: Some(value),
|
||||
dependencies: Vec::new(),
|
||||
/// An error encountered during [`AssetLoader::load`].
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AssetLoaderError {
|
||||
/// Any error that occurs during load.
|
||||
#[error(transparent)]
|
||||
Load(#[from] anyhow::Error),
|
||||
/// A failure to deserialize metadata during load.
|
||||
#[error(transparent)]
|
||||
DeserializeMeta(#[from] DeserializeMetaError),
|
||||
}
|
||||
|
||||
impl<L> ErasedAssetLoader for L
|
||||
where
|
||||
L: AssetLoader + Send + Sync,
|
||||
{
|
||||
/// Processes the asset in an asynchronous closure.
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
reader: &'a mut Reader,
|
||||
meta: Box<dyn AssetMetaDyn>,
|
||||
mut load_context: LoadContext<'a>,
|
||||
) -> BoxedFuture<'a, Result<ErasedLoadedAsset, AssetLoaderError>> {
|
||||
Box::pin(async move {
|
||||
let settings = meta
|
||||
.loader_settings()
|
||||
.expect("Loader settings should exist")
|
||||
.downcast_ref::<L::Settings>()
|
||||
.expect("AssetLoader settings should match the loader type");
|
||||
let asset = <L as AssetLoader>::load(self, reader, settings, &mut load_context).await?;
|
||||
Ok(load_context.finish(asset, Some(meta)).into())
|
||||
})
|
||||
}
|
||||
|
||||
fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError> {
|
||||
let meta = AssetMeta::<L, ()>::deserialize(meta)?;
|
||||
Ok(Box::new(meta))
|
||||
}
|
||||
|
||||
fn default_meta(&self) -> Box<dyn AssetMetaDyn> {
|
||||
Box::new(AssetMeta::<L, ()>::new(crate::meta::AssetAction::Load {
|
||||
loader: self.type_name().to_string(),
|
||||
settings: L::Settings::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &[&str] {
|
||||
<L as AssetLoader>::extensions(self)
|
||||
}
|
||||
|
||||
fn type_name(&self) -> &'static str {
|
||||
std::any::type_name::<L>()
|
||||
}
|
||||
|
||||
fn type_id(&self) -> TypeId {
|
||||
TypeId::of::<L>()
|
||||
}
|
||||
|
||||
fn asset_type_id(&self) -> TypeId {
|
||||
TypeId::of::<L::Asset>()
|
||||
}
|
||||
|
||||
fn asset_type_name(&self) -> &'static str {
|
||||
std::any::type_name::<L::Asset>()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct LabeledAsset {
|
||||
pub(crate) asset: ErasedLoadedAsset,
|
||||
pub(crate) handle: UntypedHandle,
|
||||
}
|
||||
|
||||
/// The successful result of an [`AssetLoader::load`] call. This contains the loaded "root" asset and any other "labeled" assets produced
|
||||
/// by the loader. It also holds the input [`AssetMeta`] (if it exists) and tracks dependencies:
|
||||
/// * normal dependencies: dependencies that must be loaded as part of this asset load (ex: assets a given asset has handles to).
|
||||
/// * Loader dependencies: dependencies whose actual asset values are used during the load process
|
||||
pub struct LoadedAsset<A: Asset> {
|
||||
pub(crate) value: A,
|
||||
pub(crate) dependencies: HashSet<UntypedAssetId>,
|
||||
pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
|
||||
pub(crate) labeled_assets: HashMap<String, LabeledAsset>,
|
||||
pub(crate) meta: Option<Box<dyn AssetMetaDyn>>,
|
||||
}
|
||||
|
||||
impl<A: Asset> LoadedAsset<A> {
|
||||
/// Create a new loaded asset. This will use [`VisitAssetDependencies`](crate::VisitAssetDependencies) to populate `dependencies`.
|
||||
pub fn new_with_dependencies(value: A, meta: Option<Box<dyn AssetMetaDyn>>) -> Self {
|
||||
let mut dependencies = HashSet::new();
|
||||
value.visit_dependencies(&mut |id| {
|
||||
dependencies.insert(id);
|
||||
});
|
||||
LoadedAsset {
|
||||
value,
|
||||
dependencies,
|
||||
loader_dependencies: HashMap::default(),
|
||||
labeled_assets: HashMap::default(),
|
||||
meta,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a dependency on another asset at the provided path.
|
||||
pub fn add_dependency(&mut self, asset_path: AssetPath) {
|
||||
self.dependencies.push(asset_path.to_owned());
|
||||
}
|
||||
|
||||
/// Adds a dependency on another asset at the provided path.
|
||||
#[must_use]
|
||||
pub fn with_dependency(mut self, asset_path: AssetPath) -> Self {
|
||||
self.add_dependency(asset_path);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds dependencies on other assets at the provided paths.
|
||||
#[must_use]
|
||||
pub fn with_dependencies(mut self, asset_paths: Vec<AssetPath<'static>>) -> Self {
|
||||
for asset_path in asset_paths {
|
||||
self.add_dependency(asset_path);
|
||||
}
|
||||
self
|
||||
impl<A: Asset> From<A> for LoadedAsset<A> {
|
||||
fn from(asset: A) -> Self {
|
||||
LoadedAsset::new_with_dependencies(asset, None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct BoxedLoadedAsset {
|
||||
pub(crate) value: Option<Box<dyn AssetDynamic>>,
|
||||
pub(crate) dependencies: Vec<AssetPath<'static>>,
|
||||
/// A "type erased / boxed" counterpart to [`LoadedAsset`]. This is used in places where the loaded type is not statically known.
|
||||
pub struct ErasedLoadedAsset {
|
||||
pub(crate) value: Box<dyn AssetContainer>,
|
||||
pub(crate) dependencies: HashSet<UntypedAssetId>,
|
||||
pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
|
||||
pub(crate) labeled_assets: HashMap<String, LabeledAsset>,
|
||||
pub(crate) meta: Option<Box<dyn AssetMetaDyn>>,
|
||||
}
|
||||
|
||||
impl<T: Asset> From<LoadedAsset<T>> for BoxedLoadedAsset {
|
||||
fn from(asset: LoadedAsset<T>) -> Self {
|
||||
BoxedLoadedAsset {
|
||||
value: asset
|
||||
.value
|
||||
.map(|value| Box::new(value) as Box<dyn AssetDynamic>),
|
||||
impl<A: Asset> From<LoadedAsset<A>> for ErasedLoadedAsset {
|
||||
fn from(asset: LoadedAsset<A>) -> Self {
|
||||
ErasedLoadedAsset {
|
||||
value: Box::new(asset.value),
|
||||
dependencies: asset.dependencies,
|
||||
loader_dependencies: asset.loader_dependencies,
|
||||
labeled_assets: asset.labeled_assets,
|
||||
meta: asset.meta,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An asynchronous context where an [`Asset`] is processed.
|
||||
///
|
||||
/// The load context is created by the [`AssetServer`] to process an asset source after loading its
|
||||
/// contents into memory. It is then passed to the appropriate [`AssetLoader`] based on the file
|
||||
/// extension of the asset's path.
|
||||
///
|
||||
/// An asset source can define one or more assets from a single source path. The main asset is set
|
||||
/// using [`LoadContext::set_default_asset`] and sub-assets are defined with
|
||||
/// [`LoadContext::set_labeled_asset`].
|
||||
impl ErasedLoadedAsset {
|
||||
/// Cast (and take ownership) of the [`Asset`] value of the given type. This will return [`Some`] if
|
||||
/// the stored type matches `A` and [`None`] if it does not.
|
||||
pub fn take<A: Asset>(self) -> Option<A> {
|
||||
self.value.downcast::<A>().map(|a| *a).ok()
|
||||
}
|
||||
|
||||
/// Retrieves a reference to the internal [`Asset`] type, if it matches the type `A`. Otherwise returns [`None`].
|
||||
pub fn get<A: Asset>(&self) -> Option<&A> {
|
||||
self.value.downcast_ref::<A>()
|
||||
}
|
||||
|
||||
/// Retrieves the [`TypeId`] of the stored [`Asset`] type.
|
||||
pub fn asset_type_id(&self) -> TypeId {
|
||||
(*self.value).type_id()
|
||||
}
|
||||
|
||||
/// Retrieves the `type_name` of the stored [`Asset`] type.
|
||||
pub fn asset_type_name(&self) -> &'static str {
|
||||
self.value.asset_type_name()
|
||||
}
|
||||
|
||||
/// Returns the [`ErasedLoadedAsset`] for the given label, if it exists.
|
||||
pub fn get_labeled(&self, label: &str) -> Option<&ErasedLoadedAsset> {
|
||||
self.labeled_assets.get(label).map(|a| &a.asset)
|
||||
}
|
||||
|
||||
/// Iterate over all labels for "labeled assets" in the loaded asset
|
||||
pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
|
||||
self.labeled_assets.keys().map(|s| s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// A type erased container for an [`Asset`] value that is capable of inserting the [`Asset`] into a [`World`]'s [`Assets`] collection.
|
||||
pub trait AssetContainer: Downcast + Any + Send + Sync + 'static {
|
||||
fn insert(self: Box<Self>, id: UntypedAssetId, world: &mut World);
|
||||
fn asset_type_name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
impl_downcast!(AssetContainer);
|
||||
|
||||
impl<A: Asset> AssetContainer for A {
|
||||
fn insert(self: Box<Self>, id: UntypedAssetId, world: &mut World) {
|
||||
world.resource_mut::<Assets<A>>().insert(id.typed(), *self);
|
||||
}
|
||||
|
||||
fn asset_type_name(&self) -> &'static str {
|
||||
std::any::type_name::<A>()
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that occurs when attempting to call [`LoadContext::load_direct`]
|
||||
#[derive(Error, Debug)]
|
||||
#[error("Failed to load dependency {dependency:?} {error}")]
|
||||
pub struct LoadDirectError {
|
||||
pub dependency: AssetPath<'static>,
|
||||
pub error: AssetLoadError,
|
||||
}
|
||||
|
||||
/// An error that occurs while deserializing [`AssetMeta`].
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DeserializeMetaError {
|
||||
#[error("Failed to deserialize asset meta: {0:?}")]
|
||||
DeserializeSettings(#[from] SpannedError),
|
||||
#[error("Failed to deserialize minimal asset meta: {0:?}")]
|
||||
DeserializeMinimal(SpannedError),
|
||||
}
|
||||
|
||||
/// A context that provides access to assets in [`AssetLoader`]s, tracks dependencies, and collects asset load state.
|
||||
/// Any asset state accessed by [`LoadContext`] will be tracked and stored for use in dependency events and asset preprocessing.
|
||||
pub struct LoadContext<'a> {
|
||||
pub(crate) ref_change_channel: &'a RefChangeChannel,
|
||||
pub(crate) asset_io: &'a dyn AssetIo,
|
||||
pub(crate) labeled_assets: HashMap<Option<String>, BoxedLoadedAsset>,
|
||||
pub(crate) path: &'a Path,
|
||||
pub(crate) version: usize,
|
||||
asset_server: &'a AssetServer,
|
||||
should_load_dependencies: bool,
|
||||
populate_hashes: bool,
|
||||
asset_path: AssetPath<'static>,
|
||||
dependencies: HashSet<UntypedAssetId>,
|
||||
/// Direct dependencies used by this loader.
|
||||
loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
|
||||
labeled_assets: HashMap<String, LabeledAsset>,
|
||||
}
|
||||
|
||||
impl<'a> LoadContext<'a> {
|
||||
/// Creates a new [`LoadContext`] instance.
|
||||
pub(crate) fn new(
|
||||
path: &'a Path,
|
||||
ref_change_channel: &'a RefChangeChannel,
|
||||
asset_io: &'a dyn AssetIo,
|
||||
version: usize,
|
||||
asset_server: &'a AssetServer,
|
||||
asset_path: AssetPath<'static>,
|
||||
should_load_dependencies: bool,
|
||||
populate_hashes: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
ref_change_channel,
|
||||
asset_io,
|
||||
labeled_assets: Default::default(),
|
||||
version,
|
||||
path,
|
||||
asset_server,
|
||||
asset_path,
|
||||
populate_hashes,
|
||||
should_load_dependencies,
|
||||
dependencies: HashSet::default(),
|
||||
loader_dependencies: HashMap::default(),
|
||||
labeled_assets: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Begins a new labeled asset load. Use the returned [`LoadContext`] to load
|
||||
/// dependencies for the new asset and call [`LoadContext::finish`] to finalize the asset load.
|
||||
/// When finished, make sure you call [`LoadContext::add_labeled_asset`] to add the results back to the parent
|
||||
/// context.
|
||||
/// Prefer [`LoadContext::labeled_asset_scope`] when possible, which will automatically add
|
||||
/// the labeled [`LoadContext`] back to the parent context.
|
||||
/// [`LoadContext::begin_labeled_asset`] exists largely to enable parallel asset loading.
|
||||
///
|
||||
/// See [`AssetPath`] for more on labeled assets.
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use bevy_asset::{Asset, LoadContext};
|
||||
/// # use bevy_reflect::TypePath;
|
||||
/// # #[derive(Asset, TypePath, Default)]
|
||||
/// # struct Image;
|
||||
/// # let load_context: LoadContext = panic!();
|
||||
/// let mut handles = Vec::new();
|
||||
/// for i in 0..2 {
|
||||
/// let mut labeled = load_context.begin_labeled_asset();
|
||||
/// handles.push(std::thread::spawn(move || {
|
||||
/// (i.to_string(), labeled.finish(Image::default(), None))
|
||||
/// }));
|
||||
/// }
|
||||
|
||||
/// for handle in handles {
|
||||
/// let (label, loaded_asset) = handle.join().unwrap();
|
||||
/// load_context.add_loaded_labeled_asset(label, loaded_asset);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn begin_labeled_asset(&self) -> LoadContext {
|
||||
LoadContext::new(
|
||||
self.asset_server,
|
||||
self.asset_path.clone(),
|
||||
self.should_load_dependencies,
|
||||
self.populate_hashes,
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new [`LoadContext`] for the given `label`. The `load` function is responsible for loading an [`Asset`] of
|
||||
/// type `A`. `load` will be called immediately and the result will be used to finalize the [`LoadContext`], resulting in a new
|
||||
/// [`LoadedAsset`], which is registered under the `label` label.
|
||||
///
|
||||
/// This exists to remove the need to manually call [`LoadContext::begin_labeled_asset`] and then manually register the
|
||||
/// result with [`LoadContext::add_labeled_asset`].
|
||||
///
|
||||
/// See [`AssetPath`] for more on labeled assets.
|
||||
pub fn labeled_asset_scope<A: Asset>(
|
||||
&mut self,
|
||||
label: String,
|
||||
load: impl FnOnce(&mut LoadContext) -> A,
|
||||
) -> Handle<A> {
|
||||
let mut context = self.begin_labeled_asset();
|
||||
let asset = load(&mut context);
|
||||
let loaded_asset = context.finish(asset, None);
|
||||
self.add_loaded_labeled_asset(label, loaded_asset)
|
||||
}
|
||||
|
||||
/// This will add the given `asset` as a "labeled [`Asset`]" with the `label` label.
|
||||
///
|
||||
/// See [`AssetPath`] for more on labeled assets.
|
||||
pub fn add_labeled_asset<A: Asset>(&mut self, label: String, asset: A) -> Handle<A> {
|
||||
self.labeled_asset_scope(label, |_| asset)
|
||||
}
|
||||
|
||||
/// Add a [`LoadedAsset`] that is a "labeled sub asset" of the root path of this load context.
|
||||
/// This can be used in combination with [`LoadContext::begin_labeled_asset`] to parallelize
|
||||
/// sub asset loading.
|
||||
///
|
||||
/// See [`AssetPath`] for more on labeled assets.
|
||||
pub fn add_loaded_labeled_asset<A: Asset>(
|
||||
&mut self,
|
||||
label: String,
|
||||
loaded_asset: LoadedAsset<A>,
|
||||
) -> Handle<A> {
|
||||
let loaded_asset: ErasedLoadedAsset = loaded_asset.into();
|
||||
let labeled_path = self.asset_path.with_label(label.clone());
|
||||
let handle = self
|
||||
.asset_server
|
||||
.get_or_create_path_handle(labeled_path, None);
|
||||
self.labeled_assets.insert(
|
||||
label,
|
||||
LabeledAsset {
|
||||
asset: loaded_asset,
|
||||
handle: handle.clone().untyped(),
|
||||
},
|
||||
);
|
||||
handle
|
||||
}
|
||||
|
||||
/// Returns `true` if an asset with the label `label` exists in this context.
|
||||
///
|
||||
/// See [`AssetPath`] for more on labeled assets.
|
||||
pub fn has_labeled_asset(&self, label: &str) -> bool {
|
||||
let path = self.asset_path.with_label(label);
|
||||
self.asset_server.get_handle_untyped(path).is_some()
|
||||
}
|
||||
|
||||
/// "Finishes" this context by populating the final [`Asset`] value (and the erased [`AssetMeta`] value, if it exists).
|
||||
/// The relevant asset metadata collected in this context will be stored in the returned [`LoadedAsset`].
|
||||
pub fn finish<A: Asset>(self, value: A, meta: Option<Box<dyn AssetMetaDyn>>) -> LoadedAsset<A> {
|
||||
LoadedAsset {
|
||||
value,
|
||||
dependencies: self.dependencies,
|
||||
loader_dependencies: self.loader_dependencies,
|
||||
labeled_assets: self.labeled_assets,
|
||||
meta,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the source path for this load context.
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path
|
||||
self.asset_path.path()
|
||||
}
|
||||
|
||||
/// Returns `true` if the load context contains an asset with the specified label.
|
||||
pub fn has_labeled_asset(&self, label: &str) -> bool {
|
||||
self.labeled_assets.contains_key(&Some(label.to_string()))
|
||||
/// Gets the source asset path for this load context.
|
||||
pub fn asset_path(&self) -> &AssetPath {
|
||||
&self.asset_path
|
||||
}
|
||||
|
||||
/// Sets the primary asset loaded from the asset source.
|
||||
pub fn set_default_asset<T: Asset>(&mut self, asset: LoadedAsset<T>) {
|
||||
self.labeled_assets.insert(None, asset.into());
|
||||
}
|
||||
|
||||
/// Sets a secondary asset loaded from the asset source.
|
||||
pub fn set_labeled_asset<T: Asset>(&mut self, label: &str, asset: LoadedAsset<T>) -> Handle<T> {
|
||||
assert!(!label.is_empty());
|
||||
self.labeled_assets
|
||||
.insert(Some(label.to_string()), asset.into());
|
||||
self.get_handle(AssetPath::new_ref(self.path(), Some(label)))
|
||||
}
|
||||
|
||||
/// Gets a strong handle to an asset of type `T` from its id.
|
||||
pub fn get_handle<I: Into<HandleId>, T: Asset>(&self, id: I) -> Handle<T> {
|
||||
Handle::strong(id.into(), self.ref_change_channel.sender.clone())
|
||||
}
|
||||
|
||||
/// Gets an untyped strong handle for an asset with the provided id.
|
||||
pub fn get_handle_untyped<I: Into<HandleId>>(&self, id: I) -> HandleUntyped {
|
||||
HandleUntyped::strong(id.into(), self.ref_change_channel.sender.clone())
|
||||
}
|
||||
|
||||
/// Reads the contents of the file at the specified path through the [`AssetIo`] associated
|
||||
/// with this context.
|
||||
pub async fn read_asset_bytes<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>, AssetIoError> {
|
||||
self.asset_io
|
||||
.watch_path_for_changes(path.as_ref(), Some(self.path.to_owned()))?;
|
||||
self.asset_io.load_path(path.as_ref()).await
|
||||
}
|
||||
|
||||
/// Generates metadata for the assets managed by this load context.
|
||||
pub fn get_asset_metas(&self) -> Vec<AssetMeta> {
|
||||
let mut asset_metas = Vec::new();
|
||||
for (label, asset) in &self.labeled_assets {
|
||||
asset_metas.push(AssetMeta {
|
||||
dependencies: asset.dependencies.clone(),
|
||||
label: label.clone(),
|
||||
type_uuid: asset.value.as_ref().unwrap().type_uuid(),
|
||||
});
|
||||
}
|
||||
asset_metas
|
||||
}
|
||||
|
||||
/// Gets the asset I/O associated with this load context.
|
||||
pub fn asset_io(&self) -> &dyn AssetIo {
|
||||
self.asset_io
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of loading an asset of type `T`.
|
||||
#[derive(Debug)]
|
||||
pub struct AssetResult<T> {
|
||||
/// The asset itself.
|
||||
pub asset: Box<T>,
|
||||
/// The unique id of the asset.
|
||||
pub id: HandleId,
|
||||
/// Change version.
|
||||
pub version: usize,
|
||||
}
|
||||
|
||||
/// An event channel used by asset server to update the asset storage of a `T` asset.
|
||||
#[derive(Debug)]
|
||||
pub struct AssetLifecycleChannel<T> {
|
||||
/// The sender endpoint of the channel.
|
||||
pub sender: Sender<AssetLifecycleEvent<T>>,
|
||||
/// The receiver endpoint of the channel.
|
||||
pub receiver: Receiver<AssetLifecycleEvent<T>>,
|
||||
}
|
||||
|
||||
/// Events for the [`AssetLifecycleChannel`].
|
||||
pub enum AssetLifecycleEvent<T> {
|
||||
/// An asset was created.
|
||||
Create(AssetResult<T>),
|
||||
/// An asset was freed.
|
||||
Free(HandleId),
|
||||
}
|
||||
|
||||
/// A trait for sending lifecycle notifications from assets in the asset server.
|
||||
pub trait AssetLifecycle: Downcast + Send + Sync + 'static {
|
||||
/// Notifies the asset server that a new asset was created.
|
||||
fn create_asset(&self, id: HandleId, asset: Box<dyn AssetDynamic>, version: usize);
|
||||
/// Notifies the asset server that an asset was freed.
|
||||
fn free_asset(&self, id: HandleId);
|
||||
}
|
||||
impl_downcast!(AssetLifecycle);
|
||||
|
||||
impl<T: AssetDynamic> AssetLifecycle for AssetLifecycleChannel<T> {
|
||||
fn create_asset(&self, id: HandleId, asset: Box<dyn AssetDynamic>, version: usize) {
|
||||
if let Ok(asset) = asset.downcast::<T>() {
|
||||
self.sender
|
||||
.send(AssetLifecycleEvent::Create(AssetResult {
|
||||
asset,
|
||||
id,
|
||||
version,
|
||||
}))
|
||||
.unwrap();
|
||||
/// Gets the source asset path for this load context.
|
||||
pub async fn read_asset_bytes<'b>(
|
||||
&mut self,
|
||||
path: &'b Path,
|
||||
) -> Result<Vec<u8>, ReadAssetBytesError> {
|
||||
let mut reader = self.asset_server.reader().read(path).await?;
|
||||
let hash = if self.populate_hashes {
|
||||
// NOTE: ensure meta is read while the asset bytes reader is still active to ensure transactionality
|
||||
// See `ProcessorGatedReader` for more info
|
||||
let meta_bytes = self.asset_server.reader().read_meta_bytes(path).await?;
|
||||
let minimal: ProcessedInfoMinimal = ron::de::from_bytes(&meta_bytes)
|
||||
.map_err(DeserializeMetaError::DeserializeMinimal)?;
|
||||
let processed_info = minimal
|
||||
.processed_info
|
||||
.ok_or(ReadAssetBytesError::MissingAssetHash)?;
|
||||
processed_info.full_hash
|
||||
} else {
|
||||
panic!(
|
||||
"Failed to downcast asset to {}.",
|
||||
std::any::type_name::<T>()
|
||||
);
|
||||
}
|
||||
Default::default()
|
||||
};
|
||||
let mut bytes = Vec::new();
|
||||
reader.read_to_end(&mut bytes).await?;
|
||||
self.loader_dependencies
|
||||
.insert(AssetPath::new(path.to_owned(), None), hash);
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn free_asset(&self, id: HandleId) {
|
||||
self.sender.send(AssetLifecycleEvent::Free(id)).unwrap();
|
||||
/// Retrieves a handle for the asset at the given path and adds that path as a dependency of the asset.
|
||||
/// If the current context is a normal [`AssetServer::load`], an actual asset load will be kicked off immediately, which ensures the load happens
|
||||
/// as soon as possible.
|
||||
/// "Normal loads" kicked from within a normal Bevy App will generally configure the context to kick off loads immediately.
|
||||
/// If the current context is configured to not load dependencies automatically (ex: [`AssetProcessor`](crate::processor::AssetProcessor)),
|
||||
/// a load will not be kicked off automatically. It is then the calling context's responsibility to begin a load if necessary.
|
||||
pub fn load<'b, A: Asset>(&mut self, path: impl Into<AssetPath<'b>>) -> Handle<A> {
|
||||
let path = path.into().to_owned();
|
||||
let handle = if self.should_load_dependencies {
|
||||
self.asset_server.load(path)
|
||||
} else {
|
||||
self.asset_server.get_or_create_path_handle(path, None)
|
||||
};
|
||||
self.dependencies.insert(handle.id().untyped());
|
||||
handle
|
||||
}
|
||||
|
||||
/// Loads the [`Asset`] of type `A` at the given `path` with the given [`AssetLoader::Settings`] settings `S`. This is a "deferred"
|
||||
/// load. If the settings type `S` does not match the settings expected by `A`'s asset loader, an error will be printed to the log
|
||||
/// and the asset load will fail.
|
||||
pub fn load_with_settings<'b, A: Asset, S: Settings + Default>(
|
||||
&mut self,
|
||||
path: impl Into<AssetPath<'b>>,
|
||||
settings: impl Fn(&mut S) + Send + Sync + 'static,
|
||||
) -> Handle<A> {
|
||||
let path = path.into().to_owned();
|
||||
let handle = if self.should_load_dependencies {
|
||||
self.asset_server.load_with_settings(path.clone(), settings)
|
||||
} else {
|
||||
self.asset_server.get_or_create_path_handle(
|
||||
path.clone(),
|
||||
Some(loader_settings_meta_transform(settings)),
|
||||
)
|
||||
};
|
||||
self.dependencies.insert(handle.id().untyped());
|
||||
handle
|
||||
}
|
||||
|
||||
/// Returns a handle to an asset of type `A` with the label `label`. This [`LoadContext`] must produce an asset of the
|
||||
/// given type and the given label or the dependencies of this asset will never be considered "fully loaded". However you
|
||||
/// can call this method before _or_ after adding the labeled asset.
|
||||
pub fn get_label_handle<A: Asset>(&mut self, label: &str) -> Handle<A> {
|
||||
let path = self.asset_path.with_label(label).to_owned();
|
||||
let handle = self.asset_server.get_or_create_path_handle::<A>(path, None);
|
||||
self.dependencies.insert(handle.id().untyped());
|
||||
handle
|
||||
}
|
||||
|
||||
/// Loads the asset at the given `path` directly. This is an async function that will wait until the asset is fully loaded before
|
||||
/// returning. Use this if you need the _value_ of another asset in order to load the current asset. For example, if you are
|
||||
/// deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a
|
||||
/// "load dependency".
|
||||
///
|
||||
/// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadAndSave`] preprocessor,
|
||||
/// changing a "load dependency" will result in re-processing of the asset.
|
||||
///
|
||||
/// [`Process`]: crate::processor::Process
|
||||
/// [`LoadAndSave`]: crate::processor::LoadAndSave
|
||||
pub async fn load_direct<'b>(
|
||||
&mut self,
|
||||
path: impl Into<AssetPath<'b>>,
|
||||
) -> Result<ErasedLoadedAsset, LoadDirectError> {
|
||||
let path = path.into();
|
||||
let to_error = |e: AssetLoadError| -> LoadDirectError {
|
||||
LoadDirectError {
|
||||
dependency: path.to_owned(),
|
||||
error: e,
|
||||
}
|
||||
};
|
||||
let (meta, loader, mut reader) = self
|
||||
.asset_server
|
||||
.get_meta_loader_and_reader(&path)
|
||||
.await
|
||||
.map_err(to_error)?;
|
||||
let loaded_asset = self
|
||||
.asset_server
|
||||
.load_with_meta_loader_and_reader(
|
||||
&path,
|
||||
meta,
|
||||
&*loader,
|
||||
&mut *reader,
|
||||
false,
|
||||
self.populate_hashes,
|
||||
)
|
||||
.await
|
||||
.map_err(to_error)?;
|
||||
let info = loaded_asset
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(|m| m.processed_info().as_ref());
|
||||
let hash = info.map(|i| i.full_hash).unwrap_or(Default::default());
|
||||
self.loader_dependencies.insert(path.to_owned(), hash);
|
||||
Ok(loaded_asset)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for AssetLifecycleChannel<T> {
|
||||
fn default() -> Self {
|
||||
let (sender, receiver) = crossbeam_channel::unbounded();
|
||||
AssetLifecycleChannel { sender, receiver }
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the [`Assets`] collection according to the changes queued up by [`AssetServer`].
|
||||
pub fn update_asset_storage_system<T: Asset + AssetDynamic>(
|
||||
asset_server: Res<AssetServer>,
|
||||
assets: ResMut<Assets<T>>,
|
||||
) {
|
||||
asset_server.update_asset_storage(assets);
|
||||
/// An error produced when calling [`LoadContext::read_asset_bytes`]
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ReadAssetBytesError {
|
||||
#[error(transparent)]
|
||||
DeserializeMetaError(#[from] DeserializeMetaError),
|
||||
#[error(transparent)]
|
||||
AssetReaderError(#[from] AssetReaderError),
|
||||
/// Encountered an I/O error while loading an asset.
|
||||
#[error("Encountered an io error while loading asset: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("The LoadContext for this read_asset_bytes call requires hash metadata, but it was not provided. This is likely an internal implementation error.")]
|
||||
MissingAssetHash,
|
||||
}
|
||||
|
|
250
crates/bevy_asset/src/meta.rs
Normal file
250
crates/bevy_asset/src/meta.rs
Normal file
|
@ -0,0 +1,250 @@
|
|||
use crate::{self as bevy_asset, DeserializeMetaError, VisitAssetDependencies};
|
||||
use crate::{loader::AssetLoader, processor::Process, Asset, AssetPath};
|
||||
use bevy_log::error;
|
||||
use downcast_rs::{impl_downcast, Downcast};
|
||||
use ron::ser::PrettyConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const META_FORMAT_VERSION: &str = "1.0";
|
||||
pub type MetaTransform = Box<dyn Fn(&mut dyn AssetMetaDyn) + Send + Sync>;
|
||||
|
||||
/// Asset metadata that informs how an [`Asset`] should be handled by the asset system.
|
||||
///
|
||||
/// `L` is the [`AssetLoader`] (if one is configured) for the [`AssetAction`]. This can be `()` if it is not required.
|
||||
/// `P` is the [`Process`] processor, if one is configured for the [`AssetAction`]. This can be `()` if it is not required.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AssetMeta<L: AssetLoader, P: Process> {
|
||||
/// The version of the meta format being used. This will change whenever a breaking change is made to
|
||||
/// the meta format.
|
||||
pub meta_format_version: String,
|
||||
/// Information produced by the [`AssetProcessor`] _after_ processing this asset.
|
||||
/// This will only exist alongside processed versions of assets. You should not manually set it in your asset source files.
|
||||
///
|
||||
/// [`AssetProcessor`]: crate::processor::AssetProcessor
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub processed_info: Option<ProcessedInfo>,
|
||||
/// How to handle this asset in the asset system. See [`AssetAction`].
|
||||
pub asset: AssetAction<L::Settings, P::Settings>,
|
||||
}
|
||||
|
||||
impl<L: AssetLoader, P: Process> AssetMeta<L, P> {
|
||||
pub fn new(asset: AssetAction<L::Settings, P::Settings>) -> Self {
|
||||
Self {
|
||||
meta_format_version: META_FORMAT_VERSION.to_string(),
|
||||
processed_info: None,
|
||||
asset,
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializes the given serialized byte representation of the asset meta.
|
||||
pub fn deserialize(bytes: &[u8]) -> Result<Self, DeserializeMetaError> {
|
||||
Ok(ron::de::from_bytes(bytes)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Configures how an asset source file should be handled by the asset system.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AssetAction<LoaderSettings, ProcessSettings> {
|
||||
/// Load the asset with the given loader and settings
|
||||
/// See [`AssetLoader`].
|
||||
Load {
|
||||
loader: String,
|
||||
settings: LoaderSettings,
|
||||
},
|
||||
/// Process the asset with the given processor and settings.
|
||||
/// See [`Process`] and [`AssetProcessor`].
|
||||
///
|
||||
/// [`AssetProcessor`]: crate::processor::AssetProcessor
|
||||
Process {
|
||||
processor: String,
|
||||
settings: ProcessSettings,
|
||||
},
|
||||
/// Do nothing with the asset
|
||||
Ignore,
|
||||
}
|
||||
|
||||
/// Info produced by the [`AssetProcessor`] for a given processed asset. This is used to determine if an
|
||||
/// asset source file (or its dependencies) has changed.
|
||||
///
|
||||
/// [`AssetProcessor`]: crate::processor::AssetProcessor
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
pub struct ProcessedInfo {
|
||||
/// A hash of the asset bytes and the asset .meta data
|
||||
pub hash: AssetHash,
|
||||
/// A hash of the asset bytes, the asset .meta data, and the `full_hash` of every process_dependency
|
||||
pub full_hash: AssetHash,
|
||||
/// Information about the "process dependencies" used to process this asset.
|
||||
pub process_dependencies: Vec<ProcessDependencyInfo>,
|
||||
}
|
||||
|
||||
/// Information about a dependency used to process an asset. This is used to determine whether an asset's "process dependency"
|
||||
/// has changed.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct ProcessDependencyInfo {
|
||||
pub full_hash: AssetHash,
|
||||
pub path: AssetPath<'static>,
|
||||
}
|
||||
|
||||
/// This is a minimal counterpart to [`AssetMeta`] that exists to speed up (or enable) serialization in cases where the whole [`AssetMeta`] isn't
|
||||
/// necessary.
|
||||
// PERF:
|
||||
// Currently, this is used when retrieving asset loader and processor information (when the actual type is not known yet). This could probably
|
||||
// be replaced (and made more efficient) by a custom deserializer that reads the loader/processor information _first_, then deserializes the contents
|
||||
// using a type registry.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AssetMetaMinimal {
|
||||
pub asset: AssetActionMinimal,
|
||||
}
|
||||
|
||||
/// This is a minimal counterpart to [`AssetAction`] that exists to speed up (or enable) serialization in cases where the whole [`AssetActionMinimal`]
|
||||
/// isn't necessary.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AssetActionMinimal {
|
||||
Load { loader: String },
|
||||
Process { processor: String },
|
||||
Ignore,
|
||||
}
|
||||
|
||||
/// This is a minimal counterpart to [`ProcessedInfo`] that exists to speed up serialization in cases where the whole [`ProcessedInfo`] isn't
|
||||
/// necessary.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ProcessedInfoMinimal {
|
||||
pub processed_info: Option<ProcessedInfo>,
|
||||
}
|
||||
|
||||
/// A dynamic type-erased counterpart to [`AssetMeta`] that enables passing around and interacting with [`AssetMeta`] without knowing
|
||||
/// its type.
|
||||
pub trait AssetMetaDyn: Downcast + Send + Sync {
|
||||
/// Returns a reference to the [`AssetLoader`] settings, if they exist.
|
||||
fn loader_settings(&self) -> Option<&dyn Settings>;
|
||||
/// Returns a mutable reference to the [`AssetLoader`] settings, if they exist.
|
||||
fn loader_settings_mut(&mut self) -> Option<&mut dyn Settings>;
|
||||
/// Serializes the internal [`AssetMeta`].
|
||||
fn serialize(&self) -> Vec<u8>;
|
||||
/// Returns a reference to the [`ProcessedInfo`] if it exists.
|
||||
fn processed_info(&self) -> &Option<ProcessedInfo>;
|
||||
/// Returns a mutable reference to the [`ProcessedInfo`] if it exists.
|
||||
fn processed_info_mut(&mut self) -> &mut Option<ProcessedInfo>;
|
||||
}
|
||||
|
||||
impl<L: AssetLoader, P: Process> AssetMetaDyn for AssetMeta<L, P> {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
ron::ser::to_string_pretty(&self, PrettyConfig::default())
|
||||
.expect("type is convertible to ron")
|
||||
.into_bytes()
|
||||
}
|
||||
fn loader_settings(&self) -> Option<&dyn Settings> {
|
||||
if let AssetAction::Load { settings, .. } = &self.asset {
|
||||
Some(settings)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn loader_settings_mut(&mut self) -> Option<&mut dyn Settings> {
|
||||
if let AssetAction::Load { settings, .. } = &mut self.asset {
|
||||
Some(settings)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
fn processed_info(&self) -> &Option<ProcessedInfo> {
|
||||
&self.processed_info
|
||||
}
|
||||
fn processed_info_mut(&mut self) -> &mut Option<ProcessedInfo> {
|
||||
&mut self.processed_info
|
||||
}
|
||||
}
|
||||
|
||||
impl_downcast!(AssetMetaDyn);
|
||||
|
||||
/// Settings used by the asset system, such as by [`AssetLoader`], [`Process`], and [`AssetSaver`]
|
||||
///
|
||||
/// [`AssetSaver`]: crate::saver::AssetSaver
|
||||
pub trait Settings: Downcast + Send + Sync + 'static {}
|
||||
|
||||
impl<T: 'static> Settings for T where T: Send + Sync {}
|
||||
|
||||
impl_downcast!(Settings);
|
||||
|
||||
/// The () processor should never be called. This implementation exists to make the meta format nicer to work with.
|
||||
impl Process for () {
|
||||
type Settings = ();
|
||||
type OutputLoader = ();
|
||||
|
||||
fn process<'a>(
|
||||
&'a self,
|
||||
_context: &'a mut bevy_asset::processor::ProcessContext,
|
||||
_meta: AssetMeta<(), Self>,
|
||||
_writer: &'a mut bevy_asset::io::Writer,
|
||||
) -> bevy_utils::BoxedFuture<'a, Result<(), bevy_asset::processor::ProcessError>> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Asset for () {}
|
||||
|
||||
impl VisitAssetDependencies for () {
|
||||
fn visit_dependencies(&self, _visit: &mut impl FnMut(bevy_asset::UntypedAssetId)) {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
/// The () loader should never be called. This implementation exists to make the meta format nicer to work with.
|
||||
impl AssetLoader for () {
|
||||
type Asset = ();
|
||||
type Settings = ();
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
_reader: &'a mut crate::io::Reader,
|
||||
_settings: &'a Self::Settings,
|
||||
_load_context: &'a mut crate::LoadContext,
|
||||
) -> bevy_utils::BoxedFuture<'a, Result<Self::Asset, anyhow::Error>> {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &[&str] {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn loader_settings_meta_transform<S: Settings>(
|
||||
settings: impl Fn(&mut S) + Send + Sync + 'static,
|
||||
) -> MetaTransform {
|
||||
Box::new(move |meta| {
|
||||
if let Some(loader_settings) = meta.loader_settings_mut() {
|
||||
if let Some(loader_settings) = loader_settings.downcast_mut::<S>() {
|
||||
settings(loader_settings);
|
||||
} else {
|
||||
error!(
|
||||
"Configured settings type {} does not match AssetLoader settings type",
|
||||
std::any::type_name::<S>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub type AssetHash = [u8; 16];
|
||||
|
||||
/// NOTE: changing the hashing logic here is a _breaking change_ that requires a [`META_FORMAT_VERSION`] bump.
|
||||
pub(crate) fn get_asset_hash(meta_bytes: &[u8], asset_bytes: &[u8]) -> AssetHash {
|
||||
let mut context = md5::Context::new();
|
||||
context.consume(meta_bytes);
|
||||
context.consume(asset_bytes);
|
||||
let digest = context.compute();
|
||||
digest.0
|
||||
}
|
||||
|
||||
/// NOTE: changing the hashing logic here is a _breaking change_ that requires a [`META_FORMAT_VERSION`] bump.
|
||||
pub(crate) fn get_full_asset_hash(
|
||||
asset_hash: AssetHash,
|
||||
dependency_hashes: impl Iterator<Item = AssetHash>,
|
||||
) -> AssetHash {
|
||||
let mut context = md5::Context::new();
|
||||
context.consume(asset_hash);
|
||||
for hash in dependency_hashes {
|
||||
context.consume(hash);
|
||||
}
|
||||
let digest = context.compute();
|
||||
digest.0
|
||||
}
|
|
@ -1,18 +1,59 @@
|
|||
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
|
||||
use bevy_utils::{AHasher, RandomState};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
hash::{BuildHasher, Hash, Hasher},
|
||||
fmt::{Debug, Display},
|
||||
hash::Hash,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// Represents a path to an asset in the file system.
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize, Reflect)]
|
||||
/// Represents a path to an asset in a "virtual filesystem".
|
||||
///
|
||||
/// Asset paths consist of two main parts:
|
||||
/// * [`AssetPath::path`]: The "virtual filesystem path" pointing to an asset source file.
|
||||
/// * [`AssetPath::label`]: An optional "named sub asset". When assets are loaded, they are
|
||||
/// allowed to load "sub assets" of any type, which are identified by a named "label".
|
||||
///
|
||||
/// Asset paths are generally constructed (and visualized) as strings:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use bevy_asset::{Asset, AssetServer, Handle};
|
||||
/// # use bevy_reflect::TypePath;
|
||||
/// #
|
||||
/// # #[derive(Asset, TypePath, Default)]
|
||||
/// # struct Mesh;
|
||||
/// #
|
||||
/// # #[derive(Asset, TypePath, Default)]
|
||||
/// # struct Scene;
|
||||
/// #
|
||||
/// # let asset_server: AssetServer = panic!();
|
||||
/// // This loads the `my_scene.scn` base asset.
|
||||
/// let scene: Handle<Scene> = asset_server.load("my_scene.scn");
|
||||
///
|
||||
/// // This loads the `PlayerMesh` labeled asset from the `my_scene.scn` base asset.
|
||||
/// let mesh: Handle<Mesh> = asset_server.load("my_scene.scn#PlayerMesh");
|
||||
/// ```
|
||||
#[derive(Eq, PartialEq, Hash, Clone, Serialize, Deserialize, Reflect)]
|
||||
#[reflect(Debug, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct AssetPath<'a> {
|
||||
path: Cow<'a, Path>,
|
||||
label: Option<Cow<'a, str>>,
|
||||
pub path: Cow<'a, Path>,
|
||||
pub label: Option<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl<'a> Debug for AssetPath<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Display for AssetPath<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.path.display())?;
|
||||
if let Some(label) = &self.label {
|
||||
write!(f, "#{label}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AssetPath<'a> {
|
||||
|
@ -34,24 +75,40 @@ impl<'a> AssetPath<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Constructs an identifier from this asset path.
|
||||
#[inline]
|
||||
pub fn get_id(&self) -> AssetPathId {
|
||||
AssetPathId::from(self)
|
||||
}
|
||||
|
||||
/// Gets the sub-asset label.
|
||||
/// Gets the "sub-asset label".
|
||||
#[inline]
|
||||
pub fn label(&self) -> Option<&str> {
|
||||
self.label.as_ref().map(|label| label.as_ref())
|
||||
}
|
||||
|
||||
/// Gets the path to the asset in the filesystem.
|
||||
/// Gets the path to the asset in the "virtual filesystem".
|
||||
#[inline]
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
/// Gets the path to the asset in the "virtual filesystem" without a label (if a label is currently set).
|
||||
#[inline]
|
||||
pub fn without_label(&self) -> AssetPath<'_> {
|
||||
AssetPath::new_ref(&self.path, None)
|
||||
}
|
||||
|
||||
/// Removes a "sub-asset label" from this [`AssetPath`] and returns it, if one was set.
|
||||
#[inline]
|
||||
pub fn remove_label(&mut self) -> Option<Cow<'a, str>> {
|
||||
self.label.take()
|
||||
}
|
||||
|
||||
/// Returns this asset path with the given label. This will replace the previous
|
||||
/// label if it exists.
|
||||
#[inline]
|
||||
pub fn with_label(&self, label: impl Into<Cow<'a, str>>) -> AssetPath<'a> {
|
||||
AssetPath {
|
||||
path: self.path.clone(),
|
||||
label: Some(label.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the borrowed path data to owned.
|
||||
#[inline]
|
||||
pub fn to_owned(&self) -> AssetPath<'static> {
|
||||
|
@ -63,93 +120,24 @@ impl<'a> AssetPath<'a> {
|
|||
.map(|value| Cow::Owned(value.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An unique identifier to an asset path.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize, Reflect,
|
||||
)]
|
||||
#[reflect_value(PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct AssetPathId(SourcePathId, LabelId);
|
||||
|
||||
/// An unique identifier to the source path of an asset.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize, Reflect,
|
||||
)]
|
||||
#[reflect_value(PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct SourcePathId(u64);
|
||||
|
||||
/// An unique identifier to a sub-asset label.
|
||||
#[derive(
|
||||
Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize, Reflect,
|
||||
)]
|
||||
#[reflect_value(PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct LabelId(u64);
|
||||
|
||||
impl<'a> From<&'a Path> for SourcePathId {
|
||||
fn from(value: &'a Path) -> Self {
|
||||
let mut hasher = get_hasher();
|
||||
value.hash(&mut hasher);
|
||||
SourcePathId(hasher.finish())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AssetPathId> for SourcePathId {
|
||||
fn from(id: AssetPathId) -> Self {
|
||||
id.source_path_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<AssetPath<'a>> for SourcePathId {
|
||||
fn from(path: AssetPath) -> Self {
|
||||
AssetPathId::from(path).source_path_id()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Option<&'a str>> for LabelId {
|
||||
fn from(value: Option<&'a str>) -> Self {
|
||||
let mut hasher = get_hasher();
|
||||
value.hash(&mut hasher);
|
||||
LabelId(hasher.finish())
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetPathId {
|
||||
/// Gets the id of the source path.
|
||||
pub fn source_path_id(&self) -> SourcePathId {
|
||||
self.0
|
||||
/// Returns the full extension (including multiple '.' values).
|
||||
/// Ex: Returns `"config.ron"` for `"my_asset.config.ron"`
|
||||
pub fn get_full_extension(&self) -> Option<String> {
|
||||
let file_name = self.path.file_name()?.to_str()?;
|
||||
let index = file_name.find('.')?;
|
||||
let extension = file_name[index + 1..].to_lowercase();
|
||||
Some(extension)
|
||||
}
|
||||
|
||||
/// Gets the id of the sub-asset label.
|
||||
pub fn label_id(&self) -> LabelId {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
/// this hasher provides consistent results across runs
|
||||
pub(crate) fn get_hasher() -> AHasher {
|
||||
RandomState::with_seeds(42, 23, 13, 8).build_hasher()
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for AssetPathId
|
||||
where
|
||||
T: Into<AssetPath<'a>>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
let asset_path: AssetPath = value.into();
|
||||
AssetPathId(
|
||||
SourcePathId::from(asset_path.path()),
|
||||
LabelId::from(asset_path.label()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> From<&'a AssetPath<'b>> for AssetPathId {
|
||||
fn from(asset_path: &'a AssetPath<'b>) -> Self {
|
||||
AssetPathId(
|
||||
SourcePathId::from(asset_path.path()),
|
||||
LabelId::from(asset_path.label()),
|
||||
)
|
||||
pub(crate) fn iter_secondary_extensions(full_extension: &str) -> impl Iterator<Item = &str> {
|
||||
full_extension.chars().enumerate().filter_map(|(i, c)| {
|
||||
if c == '.' {
|
||||
Some(&full_extension[i + 1..])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -189,14 +177,11 @@ impl<'a> From<PathBuf> for AssetPath<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for AssetPath<'a> {
|
||||
fn from(asset_path: String) -> Self {
|
||||
let mut parts = asset_path.splitn(2, '#');
|
||||
let path = PathBuf::from(parts.next().expect("Path must be set."));
|
||||
let label = parts.next().map(String::from);
|
||||
AssetPath {
|
||||
path: Cow::Owned(path),
|
||||
label: label.map(Cow::Owned),
|
||||
impl<'a> From<AssetPath<'a>> for PathBuf {
|
||||
fn from(path: AssetPath<'a>) -> Self {
|
||||
match path.path {
|
||||
Cow::Borrowed(borrowed) => borrowed.to_owned(),
|
||||
Cow::Owned(owned) => owned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
194
crates/bevy_asset/src/processor/log.rs
Normal file
194
crates/bevy_asset/src/processor/log.rs
Normal file
|
@ -0,0 +1,194 @@
|
|||
use async_fs::File;
|
||||
use bevy_log::error;
|
||||
use bevy_utils::HashSet;
|
||||
use futures_lite::{AsyncReadExt, AsyncWriteExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
/// An in-memory representation of a single [`ProcessorTransactionLog`] entry.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum LogEntry {
|
||||
BeginProcessing(PathBuf),
|
||||
EndProcessing(PathBuf),
|
||||
UnrecoverableError,
|
||||
}
|
||||
|
||||
/// A "write ahead" logger that helps ensure asset importing is transactional.
|
||||
/// Prior to processing an asset, we write to the log to indicate it has started
|
||||
/// After processing an asset, we write to the log to indicate it has finished.
|
||||
/// On startup, the log can be read to determine if any transactions were incomplete.
|
||||
// TODO: this should be a trait
|
||||
pub struct ProcessorTransactionLog {
|
||||
log_file: File,
|
||||
}
|
||||
|
||||
/// An error that occurs when reading from the [`ProcessorTransactionLog`] fails.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ReadLogError {
|
||||
#[error("Encountered an invalid log line: '{0}'")]
|
||||
InvalidLine(String),
|
||||
#[error("Failed to read log file: {0}")]
|
||||
Io(#[from] futures_io::Error),
|
||||
}
|
||||
|
||||
/// An error that occurs when writing to the [`ProcessorTransactionLog`] fails.
|
||||
#[derive(Error, Debug)]
|
||||
#[error(
|
||||
"Failed to write {log_entry:?} to the asset processor log. This is not recoverable. {error}"
|
||||
)]
|
||||
pub struct WriteLogError {
|
||||
log_entry: LogEntry,
|
||||
error: futures_io::Error,
|
||||
}
|
||||
|
||||
/// An error that occurs when validating the [`ProcessorTransactionLog`] fails.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ValidateLogError {
|
||||
#[error("Encountered an unrecoverable error. All assets will be reprocessed.")]
|
||||
UnrecoverableError,
|
||||
#[error(transparent)]
|
||||
ReadLogError(#[from] ReadLogError),
|
||||
#[error("Encountered a duplicate process asset transaction: {0:?}")]
|
||||
EntryErrors(Vec<LogEntryError>),
|
||||
}
|
||||
|
||||
/// An error that occurs when validating individual [`ProcessorTransactionLog`] entries.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LogEntryError {
|
||||
#[error("Encountered a duplicate process asset transaction: {0:?}")]
|
||||
DuplicateTransaction(PathBuf),
|
||||
#[error("A transaction was ended that never started {0:?}")]
|
||||
EndedMissingTransaction(PathBuf),
|
||||
#[error("An asset started processing but never finished: {0:?}")]
|
||||
UnfinishedTransaction(PathBuf),
|
||||
}
|
||||
|
||||
const LOG_PATH: &str = "imported_assets/log";
|
||||
const ENTRY_BEGIN: &str = "Begin ";
|
||||
const ENTRY_END: &str = "End ";
|
||||
const UNRECOVERABLE_ERROR: &str = "UnrecoverableError";
|
||||
|
||||
impl ProcessorTransactionLog {
|
||||
fn full_log_path() -> PathBuf {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let base_path = crate::io::file::get_base_path();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let base_path = PathBuf::new();
|
||||
base_path.join(LOG_PATH)
|
||||
}
|
||||
/// Create a new, fresh log file. This will delete the previous log file if it exists.
|
||||
pub(crate) async fn new() -> Result<Self, futures_io::Error> {
|
||||
let path = Self::full_log_path();
|
||||
match async_fs::remove_file(&path).await {
|
||||
Ok(_) => { /* successfully removed file */ }
|
||||
Err(err) => {
|
||||
// if the log file is not found, we assume we are starting in a fresh (or good) state
|
||||
if err.kind() != futures_io::ErrorKind::NotFound {
|
||||
error!("Failed to remove previous log file {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
log_file: File::create(path).await?,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn read() -> Result<Vec<LogEntry>, ReadLogError> {
|
||||
let mut log_lines = Vec::new();
|
||||
let mut file = match File::open(Self::full_log_path()).await {
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
if err.kind() == futures_io::ErrorKind::NotFound {
|
||||
// if the log file doesn't exist, this is equivalent to an empty file
|
||||
return Ok(log_lines);
|
||||
}
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
let mut string = String::new();
|
||||
file.read_to_string(&mut string).await?;
|
||||
for line in string.lines() {
|
||||
if let Some(path_str) = line.strip_prefix(ENTRY_BEGIN) {
|
||||
log_lines.push(LogEntry::BeginProcessing(PathBuf::from(path_str)));
|
||||
} else if let Some(path_str) = line.strip_prefix(ENTRY_END) {
|
||||
log_lines.push(LogEntry::EndProcessing(PathBuf::from(path_str)));
|
||||
} else if line.is_empty() {
|
||||
continue;
|
||||
} else {
|
||||
return Err(ReadLogError::InvalidLine(line.to_string()));
|
||||
}
|
||||
}
|
||||
Ok(log_lines)
|
||||
}
|
||||
|
||||
pub(crate) async fn validate() -> Result<(), ValidateLogError> {
|
||||
let mut transactions: HashSet<PathBuf> = Default::default();
|
||||
let mut errors: Vec<LogEntryError> = Vec::new();
|
||||
let entries = Self::read().await?;
|
||||
for entry in entries {
|
||||
match entry {
|
||||
LogEntry::BeginProcessing(path) => {
|
||||
// There should never be duplicate "start transactions" in a log
|
||||
// Every start should be followed by:
|
||||
// * nothing (if there was an abrupt stop)
|
||||
// * an End (if the transaction was completed)
|
||||
if !transactions.insert(path.clone()) {
|
||||
errors.push(LogEntryError::DuplicateTransaction(path));
|
||||
}
|
||||
}
|
||||
LogEntry::EndProcessing(path) => {
|
||||
if !transactions.remove(&path) {
|
||||
errors.push(LogEntryError::EndedMissingTransaction(path));
|
||||
}
|
||||
}
|
||||
LogEntry::UnrecoverableError => return Err(ValidateLogError::UnrecoverableError),
|
||||
}
|
||||
}
|
||||
for transaction in transactions {
|
||||
errors.push(LogEntryError::UnfinishedTransaction(transaction));
|
||||
}
|
||||
if !errors.is_empty() {
|
||||
return Err(ValidateLogError::EntryErrors(errors));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Logs the start of an asset being processed. If this is not followed at some point in the log by a closing [`ProcessorTransactionLog::end_processing`],
|
||||
/// in the next run of the processor the asset processing will be considered "incomplete" and it will be reprocessed.
|
||||
pub(crate) async fn begin_processing(&mut self, path: &Path) -> Result<(), WriteLogError> {
|
||||
self.write(&format!("{ENTRY_BEGIN}{}\n", path.to_string_lossy()))
|
||||
.await
|
||||
.map_err(|e| WriteLogError {
|
||||
log_entry: LogEntry::BeginProcessing(path.to_owned()),
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
|
||||
/// Logs the end of an asset being successfully processed. See [`ProcessorTransactionLog::begin_processing`].
|
||||
pub(crate) async fn end_processing(&mut self, path: &Path) -> Result<(), WriteLogError> {
|
||||
self.write(&format!("{ENTRY_END}{}\n", path.to_string_lossy()))
|
||||
.await
|
||||
.map_err(|e| WriteLogError {
|
||||
log_entry: LogEntry::EndProcessing(path.to_owned()),
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
|
||||
/// Logs an unrecoverable error. On the next run of the processor, all assets will be regenerated. This should only be used as a last resort.
|
||||
/// Every call to this should be considered with scrutiny and ideally replaced with something more granular.
|
||||
pub(crate) async fn unrecoverable(&mut self) -> Result<(), WriteLogError> {
|
||||
self.write(UNRECOVERABLE_ERROR)
|
||||
.await
|
||||
.map_err(|e| WriteLogError {
|
||||
log_entry: LogEntry::UnrecoverableError,
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
|
||||
async fn write(&mut self, line: &str) -> Result<(), futures_io::Error> {
|
||||
self.log_file.write_all(line.as_bytes()).await?;
|
||||
self.log_file.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
1269
crates/bevy_asset/src/processor/mod.rs
Normal file
1269
crates/bevy_asset/src/processor/mod.rs
Normal file
File diff suppressed because it is too large
Load diff
260
crates/bevy_asset/src/processor/process.rs
Normal file
260
crates/bevy_asset/src/processor/process.rs
Normal file
|
@ -0,0 +1,260 @@
|
|||
use crate::{
|
||||
io::{AssetReaderError, AssetWriterError, Writer},
|
||||
meta::{AssetAction, AssetMeta, AssetMetaDyn, ProcessDependencyInfo, ProcessedInfo, Settings},
|
||||
processor::AssetProcessor,
|
||||
saver::{AssetSaver, SavedAsset},
|
||||
AssetLoadError, AssetLoader, AssetPath, DeserializeMetaError, ErasedLoadedAsset,
|
||||
MissingAssetLoaderForExtensionError, MissingAssetLoaderForTypeNameError,
|
||||
};
|
||||
use bevy_utils::BoxedFuture;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{marker::PhantomData, path::PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Asset "processor" logic that reads input asset bytes (stored on [`ProcessContext`]), processes the value in some way,
|
||||
/// and then writes the final processed bytes with [`Writer`]. The resulting bytes must be loadable with the given [`Process::OutputLoader`].
|
||||
///
|
||||
/// This is a "low level", maximally flexible interface. Most use cases are better served by the [`LoadAndSave`] implementation
|
||||
/// of [`Process`].
|
||||
pub trait Process: Send + Sync + Sized + 'static {
|
||||
/// The configuration / settings used to process the asset. This will be stored in the [`AssetMeta`] and is user-configurable per-asset.
|
||||
type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
|
||||
/// The [`AssetLoader`] that will be used to load the final processed asset.
|
||||
type OutputLoader: AssetLoader;
|
||||
/// Processes the asset stored on `context` in some way using the settings stored on `meta`. The results are written to `writer`. The
|
||||
/// final written processed asset is loadable using [`Process::OutputLoader`]. This load will use the returned [`AssetLoader::Settings`].
|
||||
fn process<'a>(
|
||||
&'a self,
|
||||
context: &'a mut ProcessContext,
|
||||
meta: AssetMeta<(), Self>,
|
||||
writer: &'a mut Writer,
|
||||
) -> BoxedFuture<'a, Result<<Self::OutputLoader as AssetLoader>::Settings, ProcessError>>;
|
||||
}
|
||||
|
||||
/// A flexible [`Process`] implementation that loads the source [`Asset`] using the `L` [`AssetLoader`], then
|
||||
/// saves that `L` asset using the `S` [`AssetSaver`].
|
||||
///
|
||||
/// When creating custom processors, it is generally recommended to use the [`LoadAndSave`] [`Process`] implementation,
|
||||
/// as it encourages you to write both an [`AssetLoader`] capable of loading assets without processing enabled _and_
|
||||
/// an [`AssetSaver`] that allows you to efficiently process that asset type when that is desirable by users. However you can
|
||||
/// also implement [`Process`] directly if [`LoadAndSave`] feels limiting or unnecessary.
|
||||
///
|
||||
/// This uses [`LoadAndSaveSettings`] to configure the processor.
|
||||
///
|
||||
/// [`Asset`]: crate::Asset
|
||||
pub struct LoadAndSave<L: AssetLoader, S: AssetSaver<Asset = L::Asset>> {
|
||||
saver: S,
|
||||
marker: PhantomData<fn() -> L>,
|
||||
}
|
||||
|
||||
impl<L: AssetLoader, S: AssetSaver<Asset = L::Asset>> From<S> for LoadAndSave<L, S> {
|
||||
fn from(value: S) -> Self {
|
||||
LoadAndSave {
|
||||
saver: value,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings for the [`LoadAndSave`] [`Process::Settings`] implementation.
|
||||
///
|
||||
/// `LoaderSettings` corresponds to [`AssetLoader::Settings`] and `SaverSettings` corresponds to [`AssetSaver::Settings`].
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct LoadAndSaveSettings<LoaderSettings, SaverSettings> {
|
||||
/// The [`AssetLoader::Settings`] for [`LoadAndSave`].
|
||||
pub loader_settings: LoaderSettings,
|
||||
/// The [`AssetSaver::Settings`] for [`LoadAndSave`].
|
||||
pub saver_settings: SaverSettings,
|
||||
}
|
||||
|
||||
/// An error that is encountered during [`Process::process`].
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ProcessError {
|
||||
#[error("The asset source file for '{0}' does not exist")]
|
||||
MissingAssetSource(PathBuf),
|
||||
#[error(transparent)]
|
||||
AssetSourceIoError(std::io::Error),
|
||||
#[error(transparent)]
|
||||
MissingAssetLoaderForExtension(#[from] MissingAssetLoaderForExtensionError),
|
||||
#[error(transparent)]
|
||||
MissingAssetLoaderForTypeName(#[from] MissingAssetLoaderForTypeNameError),
|
||||
#[error("The processor '{0}' does not exist")]
|
||||
MissingProcessor(String),
|
||||
#[error(transparent)]
|
||||
AssetWriterError(#[from] AssetWriterError),
|
||||
#[error("Failed to read asset metadata {0:?}")]
|
||||
ReadAssetMetaError(AssetReaderError),
|
||||
#[error(transparent)]
|
||||
DeserializeMetaError(#[from] DeserializeMetaError),
|
||||
#[error(transparent)]
|
||||
AssetLoadError(#[from] AssetLoadError),
|
||||
#[error("The wrong meta type was passed into a processor. This is probably an internal implementation error.")]
|
||||
WrongMetaType,
|
||||
#[error("Encountered an error while saving the asset: {0}")]
|
||||
AssetSaveError(anyhow::Error),
|
||||
#[error("Assets without extensions are not supported.")]
|
||||
ExtensionRequired,
|
||||
}
|
||||
|
||||
impl<Loader: AssetLoader, Saver: AssetSaver<Asset = Loader::Asset>> Process
|
||||
for LoadAndSave<Loader, Saver>
|
||||
{
|
||||
type Settings = LoadAndSaveSettings<Loader::Settings, Saver::Settings>;
|
||||
type OutputLoader = Saver::OutputLoader;
|
||||
|
||||
fn process<'a>(
|
||||
&'a self,
|
||||
context: &'a mut ProcessContext,
|
||||
meta: AssetMeta<(), Self>,
|
||||
writer: &'a mut Writer,
|
||||
) -> BoxedFuture<'a, Result<<Self::OutputLoader as AssetLoader>::Settings, ProcessError>> {
|
||||
Box::pin(async move {
|
||||
let AssetAction::Process { settings, .. } = meta.asset else {
|
||||
return Err(ProcessError::WrongMetaType);
|
||||
};
|
||||
let loader_meta = AssetMeta::<Loader, ()>::new(AssetAction::Load {
|
||||
loader: std::any::type_name::<Loader>().to_string(),
|
||||
settings: settings.loader_settings,
|
||||
});
|
||||
let loaded_asset = context.load_source_asset(loader_meta).await?;
|
||||
let saved_asset = SavedAsset::<Loader::Asset>::from_loaded(&loaded_asset).unwrap();
|
||||
let output_settings = self
|
||||
.saver
|
||||
.save(writer, saved_asset, &settings.saver_settings)
|
||||
.await
|
||||
.map_err(ProcessError::AssetSaveError)?;
|
||||
Ok(output_settings)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erased variant of [`Process`] that enables interacting with processor implementations without knowing
|
||||
/// their type.
|
||||
pub trait ErasedProcessor: Send + Sync {
|
||||
/// Type-erased variant of [`Process::process`].
|
||||
fn process<'a>(
|
||||
&'a self,
|
||||
context: &'a mut ProcessContext,
|
||||
meta: Box<dyn AssetMetaDyn>,
|
||||
writer: &'a mut Writer,
|
||||
) -> BoxedFuture<'a, Result<Box<dyn AssetMetaDyn>, ProcessError>>;
|
||||
/// Deserialized `meta` as type-erased [`AssetMeta`], operating under the assumption that it matches the meta
|
||||
/// for the underlying [`Process`] impl.
|
||||
fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError>;
|
||||
/// Returns the default type-erased [`AssetMeta`] for the underlying [`Process`] impl.
|
||||
fn default_meta(&self) -> Box<dyn AssetMetaDyn>;
|
||||
}
|
||||
|
||||
impl<P: Process> ErasedProcessor for P {
|
||||
fn process<'a>(
|
||||
&'a self,
|
||||
context: &'a mut ProcessContext,
|
||||
meta: Box<dyn AssetMetaDyn>,
|
||||
writer: &'a mut Writer,
|
||||
) -> BoxedFuture<'a, Result<Box<dyn AssetMetaDyn>, ProcessError>> {
|
||||
Box::pin(async move {
|
||||
let meta = meta
|
||||
.downcast::<AssetMeta<(), P>>()
|
||||
.map_err(|_e| ProcessError::WrongMetaType)?;
|
||||
let loader_settings = <P as Process>::process(self, context, *meta, writer).await?;
|
||||
let output_meta: Box<dyn AssetMetaDyn> =
|
||||
Box::new(AssetMeta::<P::OutputLoader, ()>::new(AssetAction::Load {
|
||||
loader: std::any::type_name::<P::OutputLoader>().to_string(),
|
||||
settings: loader_settings,
|
||||
}));
|
||||
Ok(output_meta)
|
||||
})
|
||||
}
|
||||
|
||||
fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError> {
|
||||
let meta: AssetMeta<(), P> = ron::de::from_bytes(meta)?;
|
||||
Ok(Box::new(meta))
|
||||
}
|
||||
|
||||
fn default_meta(&self) -> Box<dyn AssetMetaDyn> {
|
||||
Box::new(AssetMeta::<(), P>::new(AssetAction::Process {
|
||||
processor: std::any::type_name::<P>().to_string(),
|
||||
settings: P::Settings::default(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides scoped data access to the [`AssetProcessor`].
|
||||
/// This must only expose processor data that is represented in the asset's hash.
|
||||
pub struct ProcessContext<'a> {
|
||||
/// The "new" processed info for the final processed asset. It is [`ProcessContext`]'s
|
||||
/// job to populate `process_dependencies` with any asset dependencies used to process
|
||||
/// this asset (ex: loading an asset value from the [`AssetServer`] of the [`AssetProcessor`])
|
||||
///
|
||||
/// DO NOT CHANGE ANY VALUES HERE OTHER THAN APPENDING TO `process_dependencies`
|
||||
///
|
||||
/// Do not expose this publicly as it would be too easily to invalidate state.
|
||||
///
|
||||
/// [`AssetServer`]: crate::server::AssetServer
|
||||
pub(crate) new_processed_info: &'a mut ProcessedInfo,
|
||||
/// This exists to expose access to asset values (via the [`AssetServer`]).
|
||||
///
|
||||
/// ANY ASSET VALUE THAT IS ACCESSED SHOULD BE ADDED TO `new_processed_info.process_dependencies`
|
||||
///
|
||||
/// Do not expose this publicly as it would be too easily to invalidate state by forgetting to update
|
||||
/// `process_dependencies`.
|
||||
///
|
||||
/// [`AssetServer`]: crate::server::AssetServer
|
||||
processor: &'a AssetProcessor,
|
||||
path: &'a AssetPath<'static>,
|
||||
asset_bytes: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> ProcessContext<'a> {
|
||||
pub(crate) fn new(
|
||||
processor: &'a AssetProcessor,
|
||||
path: &'a AssetPath<'static>,
|
||||
asset_bytes: &'a [u8],
|
||||
new_processed_info: &'a mut ProcessedInfo,
|
||||
) -> Self {
|
||||
Self {
|
||||
processor,
|
||||
path,
|
||||
asset_bytes,
|
||||
new_processed_info,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the source asset using the `L` [`AssetLoader`] and the passed in `meta` config.
|
||||
/// This will take the "load dependencies" (asset values used when loading with `L`]) and
|
||||
/// register them as "process dependencies" because they are asset values required to process the
|
||||
/// current asset.
|
||||
pub async fn load_source_asset<L: AssetLoader>(
|
||||
&mut self,
|
||||
meta: AssetMeta<L, ()>,
|
||||
) -> Result<ErasedLoadedAsset, AssetLoadError> {
|
||||
let server = &self.processor.server;
|
||||
let loader_name = std::any::type_name::<L>();
|
||||
let loader = server.get_asset_loader_with_type_name(loader_name).await?;
|
||||
let loaded_asset = server
|
||||
.load_with_meta_loader_and_reader(
|
||||
self.path,
|
||||
Box::new(meta),
|
||||
&*loader,
|
||||
&mut self.asset_bytes,
|
||||
false,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
for (path, full_hash) in loaded_asset.loader_dependencies.iter() {
|
||||
self.new_processed_info
|
||||
.process_dependencies
|
||||
.push(ProcessDependencyInfo {
|
||||
full_hash: *full_hash,
|
||||
path: path.to_owned(),
|
||||
});
|
||||
}
|
||||
Ok(loaded_asset)
|
||||
}
|
||||
|
||||
/// The source bytes of the asset being processed.
|
||||
#[inline]
|
||||
pub fn asset_bytes(&self) -> &[u8] {
|
||||
self.asset_bytes
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
use std::any::{Any, TypeId};
|
||||
|
||||
use bevy_ecs::world::{unsafe_world_cell::UnsafeWorldCell, World};
|
||||
use bevy_reflect::{FromReflect, FromType, Reflect, Uuid};
|
||||
use bevy_reflect::{FromReflect, FromType, Reflect};
|
||||
|
||||
use crate::{Asset, Assets, Handle, HandleId, HandleUntyped};
|
||||
use crate::{Asset, Assets, Handle, UntypedAssetId, UntypedHandle};
|
||||
|
||||
/// Type data for the [`TypeRegistry`](bevy_reflect::TypeRegistry) used to operate on reflected [`Asset`]s.
|
||||
///
|
||||
|
@ -11,31 +11,25 @@ use crate::{Asset, Assets, Handle, HandleId, HandleUntyped};
|
|||
/// [`add`](ReflectAsset::add) and [`remove`](ReflectAsset::remove), but can be used in situations where you don't know which asset type `T` you want
|
||||
/// until runtime.
|
||||
///
|
||||
/// [`ReflectAsset`] can be obtained via [`TypeRegistration::data`](bevy_reflect::TypeRegistration::data) if the asset was registered using [`register_asset_reflect`](crate::AddAsset::register_asset_reflect).
|
||||
/// [`ReflectAsset`] can be obtained via [`TypeRegistration::data`](bevy_reflect::TypeRegistration::data) if the asset was registered using [`register_asset_reflect`](crate::AssetApp::register_asset_reflect).
|
||||
#[derive(Clone)]
|
||||
pub struct ReflectAsset {
|
||||
type_uuid: Uuid,
|
||||
handle_type_id: TypeId,
|
||||
assets_resource_type_id: TypeId,
|
||||
|
||||
get: fn(&World, HandleUntyped) -> Option<&dyn Reflect>,
|
||||
get: fn(&World, UntypedHandle) -> Option<&dyn Reflect>,
|
||||
// SAFETY:
|
||||
// - may only be called with an [`UnsafeWorldCell`] which can be used to access the corresponding `Assets<T>` resource mutably
|
||||
// - may only be used to access **at most one** access at once
|
||||
get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, HandleUntyped) -> Option<&mut dyn Reflect>,
|
||||
add: fn(&mut World, &dyn Reflect) -> HandleUntyped,
|
||||
set: fn(&mut World, HandleUntyped, &dyn Reflect) -> HandleUntyped,
|
||||
get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedHandle) -> Option<&mut dyn Reflect>,
|
||||
add: fn(&mut World, &dyn Reflect) -> UntypedHandle,
|
||||
insert: fn(&mut World, UntypedHandle, &dyn Reflect),
|
||||
len: fn(&World) -> usize,
|
||||
ids: for<'w> fn(&'w World) -> Box<dyn Iterator<Item = HandleId> + 'w>,
|
||||
remove: fn(&mut World, HandleUntyped) -> Option<Box<dyn Reflect>>,
|
||||
ids: for<'w> fn(&'w World) -> Box<dyn Iterator<Item = UntypedAssetId> + 'w>,
|
||||
remove: fn(&mut World, UntypedHandle) -> Option<Box<dyn Reflect>>,
|
||||
}
|
||||
|
||||
impl ReflectAsset {
|
||||
/// The [`bevy_reflect::TypeUuid`] of the asset
|
||||
pub fn type_uuid(&self) -> Uuid {
|
||||
self.type_uuid
|
||||
}
|
||||
|
||||
/// The [`TypeId`] of the [`Handle<T>`] for this asset
|
||||
pub fn handle_type_id(&self) -> TypeId {
|
||||
self.handle_type_id
|
||||
|
@ -47,7 +41,7 @@ impl ReflectAsset {
|
|||
}
|
||||
|
||||
/// Equivalent of [`Assets::get`]
|
||||
pub fn get<'w>(&self, world: &'w World, handle: HandleUntyped) -> Option<&'w dyn Reflect> {
|
||||
pub fn get<'w>(&self, world: &'w World, handle: UntypedHandle) -> Option<&'w dyn Reflect> {
|
||||
(self.get)(world, handle)
|
||||
}
|
||||
|
||||
|
@ -55,7 +49,7 @@ impl ReflectAsset {
|
|||
pub fn get_mut<'w>(
|
||||
&self,
|
||||
world: &'w mut World,
|
||||
handle: HandleUntyped,
|
||||
handle: UntypedHandle,
|
||||
) -> Option<&'w mut dyn Reflect> {
|
||||
// SAFETY: unique world access
|
||||
unsafe { (self.get_unchecked_mut)(world.as_unsafe_world_cell(), handle) }
|
||||
|
@ -68,12 +62,12 @@ impl ReflectAsset {
|
|||
/// you can only have at most one alive at the same time.
|
||||
/// This means that this is *not allowed*:
|
||||
/// ```rust,no_run
|
||||
/// # use bevy_asset::{ReflectAsset, HandleUntyped};
|
||||
/// # use bevy_asset::{ReflectAsset, UntypedHandle};
|
||||
/// # use bevy_ecs::prelude::World;
|
||||
/// # let reflect_asset: ReflectAsset = unimplemented!();
|
||||
/// # let mut world: World = unimplemented!();
|
||||
/// # let handle_1: HandleUntyped = unimplemented!();
|
||||
/// # let handle_2: HandleUntyped = unimplemented!();
|
||||
/// # let handle_1: UntypedHandle = unimplemented!();
|
||||
/// # let handle_2: UntypedHandle = unimplemented!();
|
||||
/// let unsafe_world_cell = world.as_unsafe_world_cell();
|
||||
/// let a = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, handle_1).unwrap() };
|
||||
/// let b = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, handle_2).unwrap() };
|
||||
|
@ -91,28 +85,23 @@ impl ReflectAsset {
|
|||
pub unsafe fn get_unchecked_mut<'w>(
|
||||
&self,
|
||||
world: UnsafeWorldCell<'w>,
|
||||
handle: HandleUntyped,
|
||||
handle: UntypedHandle,
|
||||
) -> Option<&'w mut dyn Reflect> {
|
||||
// SAFETY: requirements are deferred to the caller
|
||||
(self.get_unchecked_mut)(world, handle)
|
||||
}
|
||||
|
||||
/// Equivalent of [`Assets::add`]
|
||||
pub fn add(&self, world: &mut World, value: &dyn Reflect) -> HandleUntyped {
|
||||
pub fn add(&self, world: &mut World, value: &dyn Reflect) -> UntypedHandle {
|
||||
(self.add)(world, value)
|
||||
}
|
||||
/// Equivalent of [`Assets::set`]
|
||||
pub fn set(
|
||||
&self,
|
||||
world: &mut World,
|
||||
handle: HandleUntyped,
|
||||
value: &dyn Reflect,
|
||||
) -> HandleUntyped {
|
||||
(self.set)(world, handle, value)
|
||||
/// Equivalent of [`Assets::insert`]
|
||||
pub fn insert(&self, world: &mut World, handle: UntypedHandle, value: &dyn Reflect) {
|
||||
(self.insert)(world, handle, value);
|
||||
}
|
||||
|
||||
/// Equivalent of [`Assets::remove`]
|
||||
pub fn remove(&self, world: &mut World, handle: HandleUntyped) -> Option<Box<dyn Reflect>> {
|
||||
pub fn remove(&self, world: &mut World, handle: UntypedHandle) -> Option<Box<dyn Reflect>> {
|
||||
(self.remove)(world, handle)
|
||||
}
|
||||
|
||||
|
@ -128,7 +117,7 @@ impl ReflectAsset {
|
|||
}
|
||||
|
||||
/// Equivalent of [`Assets::ids`]
|
||||
pub fn ids<'w>(&self, world: &'w World) -> impl Iterator<Item = HandleId> + 'w {
|
||||
pub fn ids<'w>(&self, world: &'w World) -> impl Iterator<Item = UntypedAssetId> + 'w {
|
||||
(self.ids)(world)
|
||||
}
|
||||
}
|
||||
|
@ -136,32 +125,31 @@ impl ReflectAsset {
|
|||
impl<A: Asset + FromReflect> FromType<A> for ReflectAsset {
|
||||
fn from_type() -> Self {
|
||||
ReflectAsset {
|
||||
type_uuid: A::TYPE_UUID,
|
||||
handle_type_id: TypeId::of::<Handle<A>>(),
|
||||
assets_resource_type_id: TypeId::of::<Assets<A>>(),
|
||||
get: |world, handle| {
|
||||
let assets = world.resource::<Assets<A>>();
|
||||
let asset = assets.get(&handle.typed());
|
||||
let asset = assets.get(&handle.typed_debug_checked());
|
||||
asset.map(|asset| asset as &dyn Reflect)
|
||||
},
|
||||
get_unchecked_mut: |world, handle| {
|
||||
// SAFETY: `get_unchecked_mut` must be called with `UnsafeWorldCell` having access to `Assets<A>`,
|
||||
// and must ensure to only have at most one reference to it live at all times.
|
||||
let assets = unsafe { world.get_resource_mut::<Assets<A>>().unwrap().into_inner() };
|
||||
let asset = assets.get_mut(&handle.typed());
|
||||
let asset = assets.get_mut(&handle.typed_debug_checked());
|
||||
asset.map(|asset| asset as &mut dyn Reflect)
|
||||
},
|
||||
add: |world, value| {
|
||||
let mut assets = world.resource_mut::<Assets<A>>();
|
||||
let value: A = FromReflect::from_reflect(value)
|
||||
.expect("could not call `FromReflect::from_reflect` in `ReflectAsset::add`");
|
||||
assets.add(value).into()
|
||||
assets.add(value).untyped()
|
||||
},
|
||||
set: |world, handle, value| {
|
||||
insert: |world, handle, value| {
|
||||
let mut assets = world.resource_mut::<Assets<A>>();
|
||||
let value: A = FromReflect::from_reflect(value)
|
||||
.expect("could not call `FromReflect::from_reflect` in `ReflectAsset::set`");
|
||||
assets.set(handle, value).into()
|
||||
assets.insert(handle.typed_debug_checked(), value);
|
||||
},
|
||||
len: |world| {
|
||||
let assets = world.resource::<Assets<A>>();
|
||||
|
@ -169,11 +157,11 @@ impl<A: Asset + FromReflect> FromType<A> for ReflectAsset {
|
|||
},
|
||||
ids: |world| {
|
||||
let assets = world.resource::<Assets<A>>();
|
||||
Box::new(assets.ids())
|
||||
Box::new(assets.ids().map(|i| i.untyped()))
|
||||
},
|
||||
remove: |world, handle| {
|
||||
let mut assets = world.resource_mut::<Assets<A>>();
|
||||
let value = assets.remove(handle);
|
||||
let value = assets.remove(handle.typed_debug_checked());
|
||||
value.map(|value| Box::new(value) as Box<dyn Reflect>)
|
||||
},
|
||||
}
|
||||
|
@ -207,29 +195,24 @@ impl<A: Asset + FromReflect> FromType<A> for ReflectAsset {
|
|||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct ReflectHandle {
|
||||
type_uuid: Uuid,
|
||||
asset_type_id: TypeId,
|
||||
downcast_handle_untyped: fn(&dyn Any) -> Option<HandleUntyped>,
|
||||
typed: fn(HandleUntyped) -> Box<dyn Reflect>,
|
||||
downcast_handle_untyped: fn(&dyn Any) -> Option<UntypedHandle>,
|
||||
typed: fn(UntypedHandle) -> Box<dyn Reflect>,
|
||||
}
|
||||
impl ReflectHandle {
|
||||
/// The [`bevy_reflect::TypeUuid`] of the asset
|
||||
pub fn type_uuid(&self) -> Uuid {
|
||||
self.type_uuid
|
||||
}
|
||||
/// The [`TypeId`] of the asset
|
||||
pub fn asset_type_id(&self) -> TypeId {
|
||||
self.asset_type_id
|
||||
}
|
||||
|
||||
/// A way to go from a [`Handle<T>`] in a `dyn Any` to a [`HandleUntyped`]
|
||||
pub fn downcast_handle_untyped(&self, handle: &dyn Any) -> Option<HandleUntyped> {
|
||||
/// A way to go from a [`Handle<T>`] in a `dyn Any` to a [`UntypedHandle`]
|
||||
pub fn downcast_handle_untyped(&self, handle: &dyn Any) -> Option<UntypedHandle> {
|
||||
(self.downcast_handle_untyped)(handle)
|
||||
}
|
||||
|
||||
/// A way to go from a [`HandleUntyped`] to a [`Handle<T>`] in a `Box<dyn Reflect>`.
|
||||
/// Equivalent of [`HandleUntyped::typed`].
|
||||
pub fn typed(&self, handle: HandleUntyped) -> Box<dyn Reflect> {
|
||||
/// A way to go from a [`UntypedHandle`] to a [`Handle<T>`] in a `Box<dyn Reflect>`.
|
||||
/// Equivalent of [`UntypedHandle::typed`].
|
||||
pub fn typed(&self, handle: UntypedHandle) -> Box<dyn Reflect> {
|
||||
(self.typed)(handle)
|
||||
}
|
||||
}
|
||||
|
@ -237,14 +220,13 @@ impl ReflectHandle {
|
|||
impl<A: Asset> FromType<Handle<A>> for ReflectHandle {
|
||||
fn from_type() -> Self {
|
||||
ReflectHandle {
|
||||
type_uuid: A::TYPE_UUID,
|
||||
asset_type_id: TypeId::of::<A>(),
|
||||
downcast_handle_untyped: |handle: &dyn Any| {
|
||||
handle
|
||||
.downcast_ref::<Handle<A>>()
|
||||
.map(|handle| handle.clone_untyped())
|
||||
.map(|h| h.clone().untyped())
|
||||
},
|
||||
typed: |handle: HandleUntyped| Box::new(handle.typed::<A>()),
|
||||
typed: |handle: UntypedHandle| Box::new(handle.typed_debug_checked::<A>()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -253,14 +235,13 @@ impl<A: Asset> FromType<Handle<A>> for ReflectHandle {
|
|||
mod tests {
|
||||
use std::any::TypeId;
|
||||
|
||||
use crate as bevy_asset;
|
||||
use crate::{Asset, AssetApp, AssetPlugin, ReflectAsset, UntypedHandle};
|
||||
use bevy_app::App;
|
||||
use bevy_ecs::reflect::AppTypeRegistry;
|
||||
use bevy_reflect::{Reflect, ReflectMut, TypeUuid};
|
||||
use bevy_reflect::{Reflect, ReflectMut};
|
||||
|
||||
use crate::{AddAsset, AssetPlugin, HandleUntyped, ReflectAsset};
|
||||
|
||||
#[derive(Reflect, TypeUuid)]
|
||||
#[uuid = "09191350-1238-4736-9a89-46f04bda6966"]
|
||||
#[derive(Asset, Reflect)]
|
||||
struct AssetType {
|
||||
field: String,
|
||||
}
|
||||
|
@ -269,7 +250,7 @@ mod tests {
|
|||
fn test_reflect_asset_operations() {
|
||||
let mut app = App::new();
|
||||
app.add_plugins(AssetPlugin::default())
|
||||
.add_asset::<AssetType>()
|
||||
.init_asset::<AssetType>()
|
||||
.register_asset_reflect::<AssetType>();
|
||||
|
||||
let reflect_asset = {
|
||||
|
@ -304,7 +285,7 @@ mod tests {
|
|||
let ids: Vec<_> = reflect_asset.ids(&app.world).collect();
|
||||
assert_eq!(ids.len(), 1);
|
||||
|
||||
let fetched_handle = HandleUntyped::weak(ids[0]);
|
||||
let fetched_handle = UntypedHandle::Weak(ids[0]);
|
||||
let asset = reflect_asset
|
||||
.get(&app.world, fetched_handle.clone_weak())
|
||||
.unwrap();
|
||||
|
|
110
crates/bevy_asset/src/saver.rs
Normal file
110
crates/bevy_asset/src/saver.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
use crate::{io::Writer, meta::Settings, Asset, ErasedLoadedAsset};
|
||||
use crate::{AssetLoader, LabeledAsset};
|
||||
use bevy_utils::{BoxedFuture, HashMap};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
|
||||
/// Saves an [`Asset`] of a given [`AssetSaver::Asset`] type. [`AssetSaver::OutputLoader`] will then be used to load the saved asset
|
||||
/// in the final deployed application. The saver should produce asset bytes in a format that [`AssetSaver::OutputLoader`] can read.
|
||||
pub trait AssetSaver: Send + Sync + 'static {
|
||||
type Asset: Asset;
|
||||
type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
|
||||
type OutputLoader: AssetLoader;
|
||||
|
||||
/// Saves the given runtime [`Asset`] by writing it to a byte format using `writer`. The passed in `settings` can influence how the
|
||||
/// `asset` is saved.
|
||||
fn save<'a>(
|
||||
&'a self,
|
||||
writer: &'a mut Writer,
|
||||
asset: SavedAsset<'a, Self::Asset>,
|
||||
settings: &'a Self::Settings,
|
||||
) -> BoxedFuture<'a, Result<<Self::OutputLoader as AssetLoader>::Settings, anyhow::Error>>;
|
||||
}
|
||||
|
||||
/// A type-erased dynamic variant of [`AssetSaver`] that allows callers to save assets without knowing the actual type of the [`AssetSaver`].
|
||||
pub trait ErasedAssetSaver: Send + Sync + 'static {
|
||||
/// Saves the given runtime [`ErasedLoadedAsset`] by writing it to a byte format using `writer`. The passed in `settings` can influence how the
|
||||
/// `asset` is saved.
|
||||
fn save<'a>(
|
||||
&'a self,
|
||||
writer: &'a mut Writer,
|
||||
asset: &'a ErasedLoadedAsset,
|
||||
settings: &'a dyn Settings,
|
||||
) -> BoxedFuture<'a, Result<(), anyhow::Error>>;
|
||||
|
||||
/// The type name of the [`AssetSaver`].
|
||||
fn type_name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
impl<S: AssetSaver> ErasedAssetSaver for S {
|
||||
fn save<'a>(
|
||||
&'a self,
|
||||
writer: &'a mut Writer,
|
||||
asset: &'a ErasedLoadedAsset,
|
||||
settings: &'a dyn Settings,
|
||||
) -> BoxedFuture<'a, Result<(), anyhow::Error>> {
|
||||
Box::pin(async move {
|
||||
let settings = settings
|
||||
.downcast_ref::<S::Settings>()
|
||||
.expect("AssetLoader settings should match the loader type");
|
||||
let saved_asset = SavedAsset::<S::Asset>::from_loaded(asset).unwrap();
|
||||
self.save(writer, saved_asset, settings).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
fn type_name(&self) -> &'static str {
|
||||
std::any::type_name::<S>()
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`Asset`] (and any labeled "sub assets") intended to be saved.
|
||||
pub struct SavedAsset<'a, A: Asset> {
|
||||
value: &'a A,
|
||||
labeled_assets: &'a HashMap<String, LabeledAsset>,
|
||||
}
|
||||
|
||||
impl<'a, A: Asset> Deref for SavedAsset<'a, A> {
|
||||
type Target = A;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, A: Asset> SavedAsset<'a, A> {
|
||||
/// Creates a new [`SavedAsset`] from `asset` if its internal value matches `A`.
|
||||
pub fn from_loaded(asset: &'a ErasedLoadedAsset) -> Option<Self> {
|
||||
let value = asset.value.downcast_ref::<A>()?;
|
||||
Some(SavedAsset {
|
||||
value,
|
||||
labeled_assets: &asset.labeled_assets,
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieves the value of this asset.
|
||||
#[inline]
|
||||
pub fn get(&self) -> &'a A {
|
||||
self.value
|
||||
}
|
||||
|
||||
/// Returns the labeled asset, if it exists and matches this type.
|
||||
pub fn get_labeled<B: Asset>(&self, label: &str) -> Option<SavedAsset<B>> {
|
||||
let labeled = self.labeled_assets.get(label)?;
|
||||
let value = labeled.asset.value.downcast_ref::<B>()?;
|
||||
Some(SavedAsset {
|
||||
value,
|
||||
labeled_assets: &labeled.asset.labeled_assets,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the type-erased labeled asset, if it exists and matches this type.
|
||||
pub fn get_erased_labeled(&self, label: &str) -> Option<&ErasedLoadedAsset> {
|
||||
let labeled = self.labeled_assets.get(label)?;
|
||||
Some(&labeled.asset)
|
||||
}
|
||||
|
||||
/// Iterate over all labels for "labeled assets" in the loaded asset
|
||||
pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
|
||||
self.labeled_assets.keys().map(|s| s.as_str())
|
||||
}
|
||||
}
|
603
crates/bevy_asset/src/server/info.rs
Normal file
603
crates/bevy_asset/src/server/info.rs
Normal file
|
@ -0,0 +1,603 @@
|
|||
use crate::{
|
||||
meta::{AssetHash, MetaTransform},
|
||||
Asset, AssetHandleProvider, AssetPath, DependencyLoadState, ErasedLoadedAsset, Handle,
|
||||
InternalAssetEvent, LoadState, RecursiveDependencyLoadState, StrongHandle, UntypedAssetId,
|
||||
UntypedHandle,
|
||||
};
|
||||
use bevy_ecs::world::World;
|
||||
use bevy_log::warn;
|
||||
use bevy_utils::{Entry, HashMap, HashSet};
|
||||
use crossbeam_channel::Sender;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AssetInfo {
|
||||
weak_handle: Weak<StrongHandle>,
|
||||
pub(crate) path: Option<AssetPath<'static>>,
|
||||
pub(crate) load_state: LoadState,
|
||||
pub(crate) dep_load_state: DependencyLoadState,
|
||||
pub(crate) rec_dep_load_state: RecursiveDependencyLoadState,
|
||||
loading_dependencies: HashSet<UntypedAssetId>,
|
||||
failed_dependencies: HashSet<UntypedAssetId>,
|
||||
loading_rec_dependencies: HashSet<UntypedAssetId>,
|
||||
failed_rec_dependencies: HashSet<UntypedAssetId>,
|
||||
dependants_waiting_on_load: HashSet<UntypedAssetId>,
|
||||
dependants_waiting_on_recursive_dep_load: HashSet<UntypedAssetId>,
|
||||
/// The asset paths required to load this asset. Hashes will only be set for processed assets.
|
||||
/// This is set using the value from [`LoadedAsset`].
|
||||
/// This will only be populated if [`AssetInfos::watching_for_changes`] is set to `true` to
|
||||
/// save memory.
|
||||
///
|
||||
/// [`LoadedAsset`]: crate::loader::LoadedAsset
|
||||
loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
|
||||
/// The number of handle drops to skip for this asset.
|
||||
/// See usage (and comments) in get_or_create_path_handle for context.
|
||||
handle_drops_to_skip: usize,
|
||||
}
|
||||
|
||||
impl AssetInfo {
|
||||
fn new(weak_handle: Weak<StrongHandle>, path: Option<AssetPath<'static>>) -> Self {
|
||||
Self {
|
||||
weak_handle,
|
||||
path,
|
||||
load_state: LoadState::NotLoaded,
|
||||
dep_load_state: DependencyLoadState::NotLoaded,
|
||||
rec_dep_load_state: RecursiveDependencyLoadState::NotLoaded,
|
||||
loading_dependencies: HashSet::default(),
|
||||
failed_dependencies: HashSet::default(),
|
||||
loading_rec_dependencies: HashSet::default(),
|
||||
failed_rec_dependencies: HashSet::default(),
|
||||
loader_dependencies: HashMap::default(),
|
||||
dependants_waiting_on_load: HashSet::default(),
|
||||
dependants_waiting_on_recursive_dep_load: HashSet::default(),
|
||||
handle_drops_to_skip: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct AssetInfos {
|
||||
path_to_id: HashMap<AssetPath<'static>, UntypedAssetId>,
|
||||
infos: HashMap<UntypedAssetId, AssetInfo>,
|
||||
/// If set to `true`, this informs [`AssetInfos`] to track data relevant to watching for changes (such as `load_dependants`)
|
||||
/// This should only be set at startup.
|
||||
pub(crate) watching_for_changes: bool,
|
||||
/// Tracks assets that depend on the "key" asset path inside their asset loaders ("loader dependencies")
|
||||
/// This should only be set when watching for changes to avoid unnecessary work.
|
||||
pub(crate) loader_dependants: HashMap<AssetPath<'static>, HashSet<AssetPath<'static>>>,
|
||||
pub(crate) handle_providers: HashMap<TypeId, AssetHandleProvider>,
|
||||
pub(crate) dependency_loaded_event_sender: HashMap<TypeId, fn(&mut World, UntypedAssetId)>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AssetInfos {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AssetInfos")
|
||||
.field("path_to_id", &self.path_to_id)
|
||||
.field("infos", &self.infos)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetInfos {
|
||||
pub(crate) fn create_loading_handle<A: Asset>(&mut self) -> Handle<A> {
|
||||
unwrap_with_context(
|
||||
Self::create_handle_internal(
|
||||
&mut self.infos,
|
||||
&self.handle_providers,
|
||||
TypeId::of::<A>(),
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
std::any::type_name::<A>(),
|
||||
)
|
||||
.typed_debug_checked()
|
||||
}
|
||||
|
||||
pub(crate) fn create_loading_handle_untyped(
|
||||
&mut self,
|
||||
type_id: TypeId,
|
||||
type_name: &'static str,
|
||||
) -> UntypedHandle {
|
||||
unwrap_with_context(
|
||||
Self::create_handle_internal(
|
||||
&mut self.infos,
|
||||
&self.handle_providers,
|
||||
type_id,
|
||||
None,
|
||||
None,
|
||||
true,
|
||||
),
|
||||
type_name,
|
||||
)
|
||||
}
|
||||
|
||||
fn create_handle_internal(
|
||||
infos: &mut HashMap<UntypedAssetId, AssetInfo>,
|
||||
handle_providers: &HashMap<TypeId, AssetHandleProvider>,
|
||||
type_id: TypeId,
|
||||
path: Option<AssetPath<'static>>,
|
||||
meta_transform: Option<MetaTransform>,
|
||||
loading: bool,
|
||||
) -> Result<UntypedHandle, MissingHandleProviderError> {
|
||||
let provider = handle_providers
|
||||
.get(&type_id)
|
||||
.ok_or(MissingHandleProviderError(type_id))?;
|
||||
|
||||
let handle = provider.reserve_handle_internal(true, path.clone(), meta_transform);
|
||||
let mut info = AssetInfo::new(Arc::downgrade(&handle), path);
|
||||
if loading {
|
||||
info.load_state = LoadState::Loading;
|
||||
info.dep_load_state = DependencyLoadState::Loading;
|
||||
info.rec_dep_load_state = RecursiveDependencyLoadState::Loading;
|
||||
}
|
||||
infos.insert(handle.id, info);
|
||||
Ok(UntypedHandle::Strong(handle))
|
||||
}
|
||||
|
||||
pub(crate) fn get_or_create_path_handle<A: Asset>(
|
||||
&mut self,
|
||||
path: AssetPath<'static>,
|
||||
loading_mode: HandleLoadingMode,
|
||||
meta_transform: Option<MetaTransform>,
|
||||
) -> (Handle<A>, bool) {
|
||||
let result = self.get_or_create_path_handle_internal(
|
||||
path,
|
||||
TypeId::of::<A>(),
|
||||
loading_mode,
|
||||
meta_transform,
|
||||
);
|
||||
let (handle, should_load) = unwrap_with_context(result, std::any::type_name::<A>());
|
||||
(handle.typed_unchecked(), should_load)
|
||||
}
|
||||
|
||||
pub(crate) fn get_or_create_path_handle_untyped(
|
||||
&mut self,
|
||||
path: AssetPath<'static>,
|
||||
type_id: TypeId,
|
||||
type_name: &'static str,
|
||||
loading_mode: HandleLoadingMode,
|
||||
meta_transform: Option<MetaTransform>,
|
||||
) -> (UntypedHandle, bool) {
|
||||
let result =
|
||||
self.get_or_create_path_handle_internal(path, type_id, loading_mode, meta_transform);
|
||||
unwrap_with_context(result, type_name)
|
||||
}
|
||||
|
||||
/// Retrieves asset tracking data, or creates it if it doesn't exist.
|
||||
/// Returns true if an asset load should be kicked off
|
||||
pub fn get_or_create_path_handle_internal(
|
||||
&mut self,
|
||||
path: AssetPath<'static>,
|
||||
type_id: TypeId,
|
||||
loading_mode: HandleLoadingMode,
|
||||
meta_transform: Option<MetaTransform>,
|
||||
) -> Result<(UntypedHandle, bool), MissingHandleProviderError> {
|
||||
match self.path_to_id.entry(path.clone()) {
|
||||
Entry::Occupied(entry) => {
|
||||
let id = *entry.get();
|
||||
// if there is a path_to_id entry, info always exists
|
||||
let info = self.infos.get_mut(&id).unwrap();
|
||||
let mut should_load = false;
|
||||
if loading_mode == HandleLoadingMode::Force
|
||||
|| (loading_mode == HandleLoadingMode::Request
|
||||
&& info.load_state == LoadState::NotLoaded)
|
||||
{
|
||||
info.load_state = LoadState::Loading;
|
||||
info.dep_load_state = DependencyLoadState::Loading;
|
||||
info.rec_dep_load_state = RecursiveDependencyLoadState::Loading;
|
||||
should_load = true;
|
||||
}
|
||||
|
||||
if let Some(strong_handle) = info.weak_handle.upgrade() {
|
||||
// If we can upgrade the handle, there is at least one live handle right now,
|
||||
// The asset load has already kicked off (and maybe completed), so we can just
|
||||
// return a strong handle
|
||||
Ok((UntypedHandle::Strong(strong_handle), should_load))
|
||||
} else {
|
||||
// Asset meta exists, but all live handles were dropped. This means the `track_assets` system
|
||||
// hasn't been run yet to remove the current asset
|
||||
// (note that this is guaranteed to be transactional with the `track_assets` system because
|
||||
// because it locks the AssetInfos collection)
|
||||
|
||||
// We must create a new strong handle for the existing id and ensure that the drop of the old
|
||||
// strong handle doesn't remove the asset from the Assets collection
|
||||
info.handle_drops_to_skip += 1;
|
||||
let provider = self
|
||||
.handle_providers
|
||||
.get(&type_id)
|
||||
.ok_or(MissingHandleProviderError(type_id))?;
|
||||
let handle =
|
||||
provider.get_handle(id.internal(), true, Some(path), meta_transform);
|
||||
info.weak_handle = Arc::downgrade(&handle);
|
||||
Ok((UntypedHandle::Strong(handle), should_load))
|
||||
}
|
||||
}
|
||||
// The entry does not exist, so this is a "fresh" asset load. We must create a new handle
|
||||
Entry::Vacant(entry) => {
|
||||
let should_load = match loading_mode {
|
||||
HandleLoadingMode::NotLoading => false,
|
||||
HandleLoadingMode::Request | HandleLoadingMode::Force => true,
|
||||
};
|
||||
let handle = Self::create_handle_internal(
|
||||
&mut self.infos,
|
||||
&self.handle_providers,
|
||||
type_id,
|
||||
Some(path),
|
||||
meta_transform,
|
||||
should_load,
|
||||
)?;
|
||||
entry.insert(handle.id());
|
||||
Ok((handle, should_load))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, id: UntypedAssetId) -> Option<&AssetInfo> {
|
||||
self.infos.get(&id)
|
||||
}
|
||||
|
||||
pub(crate) fn get_mut(&mut self, id: UntypedAssetId) -> Option<&mut AssetInfo> {
|
||||
self.infos.get_mut(&id)
|
||||
}
|
||||
|
||||
pub(crate) fn get_path_handle(&self, path: AssetPath) -> Option<UntypedHandle> {
|
||||
let id = *self.path_to_id.get(&path)?;
|
||||
self.get_id_handle(id)
|
||||
}
|
||||
|
||||
pub(crate) fn get_id_handle(&self, id: UntypedAssetId) -> Option<UntypedHandle> {
|
||||
let info = self.infos.get(&id)?;
|
||||
let strong_handle = info.weak_handle.upgrade()?;
|
||||
Some(UntypedHandle::Strong(strong_handle))
|
||||
}
|
||||
|
||||
/// Returns `true` if this path has
|
||||
pub(crate) fn is_path_alive(&self, path: &AssetPath) -> bool {
|
||||
if let Some(id) = self.path_to_id.get(path) {
|
||||
if let Some(info) = self.infos.get(id) {
|
||||
return info.weak_handle.strong_count() > 0;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// Returns `true` if the asset should be removed from the collection
|
||||
pub(crate) fn process_handle_drop(&mut self, id: UntypedAssetId) -> bool {
|
||||
Self::process_handle_drop_internal(
|
||||
&mut self.infos,
|
||||
&mut self.path_to_id,
|
||||
&mut self.loader_dependants,
|
||||
self.watching_for_changes,
|
||||
id,
|
||||
)
|
||||
}
|
||||
|
||||
/// Updates [`AssetInfo`] / load state for an asset that has finished loading (and relevant dependencies / dependants).
|
||||
pub(crate) fn process_asset_load(
|
||||
&mut self,
|
||||
loaded_asset_id: UntypedAssetId,
|
||||
loaded_asset: ErasedLoadedAsset,
|
||||
world: &mut World,
|
||||
sender: &Sender<InternalAssetEvent>,
|
||||
) {
|
||||
loaded_asset.value.insert(loaded_asset_id, world);
|
||||
let mut loading_deps = loaded_asset.dependencies;
|
||||
let mut failed_deps = HashSet::new();
|
||||
let mut loading_rec_deps = loading_deps.clone();
|
||||
let mut failed_rec_deps = HashSet::new();
|
||||
loading_deps.retain(|dep_id| {
|
||||
if let Some(dep_info) = self.get_mut(*dep_id) {
|
||||
match dep_info.rec_dep_load_state {
|
||||
RecursiveDependencyLoadState::Loading
|
||||
| RecursiveDependencyLoadState::NotLoaded => {
|
||||
// If dependency is loading, wait for it.
|
||||
dep_info
|
||||
.dependants_waiting_on_recursive_dep_load
|
||||
.insert(loaded_asset_id);
|
||||
}
|
||||
RecursiveDependencyLoadState::Loaded => {
|
||||
// If dependency is loaded, reduce our count by one
|
||||
loading_rec_deps.remove(dep_id);
|
||||
}
|
||||
RecursiveDependencyLoadState::Failed => {
|
||||
failed_rec_deps.insert(*dep_id);
|
||||
loading_rec_deps.remove(dep_id);
|
||||
}
|
||||
}
|
||||
match dep_info.load_state {
|
||||
LoadState::NotLoaded | LoadState::Loading => {
|
||||
// If dependency is loading, wait for it.
|
||||
dep_info.dependants_waiting_on_load.insert(loaded_asset_id);
|
||||
true
|
||||
}
|
||||
LoadState::Loaded => {
|
||||
// If dependency is loaded, reduce our count by one
|
||||
false
|
||||
}
|
||||
LoadState::Failed => {
|
||||
failed_deps.insert(*dep_id);
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// the dependency id does not exist, which implies it was manually removed or never existed in the first place
|
||||
warn!(
|
||||
"Dependency {:?} from asset {:?} is unknown. This asset's dependency load status will not switch to 'Loaded' until the unknown dependency is loaded.",
|
||||
dep_id, loaded_asset_id
|
||||
);
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
let dep_load_state = match (loading_deps.len(), failed_deps.len()) {
|
||||
(0, 0) => DependencyLoadState::Loaded,
|
||||
(_loading, 0) => DependencyLoadState::Loading,
|
||||
(_loading, _failed) => DependencyLoadState::Failed,
|
||||
};
|
||||
|
||||
let rec_dep_load_state = match (loading_rec_deps.len(), failed_rec_deps.len()) {
|
||||
(0, 0) => {
|
||||
sender
|
||||
.send(InternalAssetEvent::LoadedWithDependencies {
|
||||
id: loaded_asset_id,
|
||||
})
|
||||
.unwrap();
|
||||
RecursiveDependencyLoadState::Loaded
|
||||
}
|
||||
(_loading, 0) => RecursiveDependencyLoadState::Loading,
|
||||
(_loading, _failed) => RecursiveDependencyLoadState::Failed,
|
||||
};
|
||||
|
||||
let (dependants_waiting_on_load, dependants_waiting_on_rec_load) = {
|
||||
let watching_for_changes = self.watching_for_changes;
|
||||
// if watching for changes, track reverse loader dependencies for hot reloading
|
||||
if watching_for_changes {
|
||||
let info = self
|
||||
.infos
|
||||
.get(&loaded_asset_id)
|
||||
.expect("Asset info should always exist at this point");
|
||||
if let Some(asset_path) = &info.path {
|
||||
for loader_dependency in loaded_asset.loader_dependencies.keys() {
|
||||
let dependants = self
|
||||
.loader_dependants
|
||||
.entry(loader_dependency.clone())
|
||||
.or_default();
|
||||
dependants.insert(asset_path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
let info = self
|
||||
.get_mut(loaded_asset_id)
|
||||
.expect("Asset info should always exist at this point");
|
||||
info.loading_dependencies = loading_deps;
|
||||
info.failed_dependencies = failed_deps;
|
||||
info.loading_rec_dependencies = loading_rec_deps;
|
||||
info.failed_rec_dependencies = failed_rec_deps;
|
||||
info.load_state = LoadState::Loaded;
|
||||
info.dep_load_state = dep_load_state;
|
||||
info.rec_dep_load_state = rec_dep_load_state;
|
||||
if watching_for_changes {
|
||||
info.loader_dependencies = loaded_asset.loader_dependencies;
|
||||
}
|
||||
|
||||
let dependants_waiting_on_rec_load = if matches!(
|
||||
rec_dep_load_state,
|
||||
RecursiveDependencyLoadState::Loaded | RecursiveDependencyLoadState::Failed
|
||||
) {
|
||||
Some(std::mem::take(
|
||||
&mut info.dependants_waiting_on_recursive_dep_load,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
(
|
||||
std::mem::take(&mut info.dependants_waiting_on_load),
|
||||
dependants_waiting_on_rec_load,
|
||||
)
|
||||
};
|
||||
|
||||
for id in dependants_waiting_on_load {
|
||||
if let Some(info) = self.get_mut(id) {
|
||||
info.loading_dependencies.remove(&loaded_asset_id);
|
||||
if info.loading_dependencies.is_empty() {
|
||||
// send dependencies loaded event
|
||||
info.dep_load_state = DependencyLoadState::Loaded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(dependants_waiting_on_rec_load) = dependants_waiting_on_rec_load {
|
||||
match rec_dep_load_state {
|
||||
RecursiveDependencyLoadState::Loaded => {
|
||||
for dep_id in dependants_waiting_on_rec_load {
|
||||
Self::propagate_loaded_state(self, loaded_asset_id, dep_id, sender);
|
||||
}
|
||||
}
|
||||
RecursiveDependencyLoadState::Failed => {
|
||||
for dep_id in dependants_waiting_on_rec_load {
|
||||
Self::propagate_failed_state(self, loaded_asset_id, dep_id);
|
||||
}
|
||||
}
|
||||
RecursiveDependencyLoadState::Loading | RecursiveDependencyLoadState::NotLoaded => {
|
||||
// dependants_waiting_on_rec_load should be None in this case
|
||||
unreachable!("`Loading` and `NotLoaded` state should never be propagated.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively propagates loaded state up the dependency tree.
|
||||
fn propagate_loaded_state(
|
||||
infos: &mut AssetInfos,
|
||||
loaded_id: UntypedAssetId,
|
||||
waiting_id: UntypedAssetId,
|
||||
sender: &Sender<InternalAssetEvent>,
|
||||
) {
|
||||
let dependants_waiting_on_rec_load = if let Some(info) = infos.get_mut(waiting_id) {
|
||||
info.loading_rec_dependencies.remove(&loaded_id);
|
||||
if info.loading_rec_dependencies.is_empty() && info.failed_rec_dependencies.is_empty() {
|
||||
info.rec_dep_load_state = RecursiveDependencyLoadState::Loaded;
|
||||
if info.load_state == LoadState::Loaded {
|
||||
sender
|
||||
.send(InternalAssetEvent::LoadedWithDependencies { id: waiting_id })
|
||||
.unwrap();
|
||||
}
|
||||
Some(std::mem::take(
|
||||
&mut info.dependants_waiting_on_recursive_dep_load,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(dependants_waiting_on_rec_load) = dependants_waiting_on_rec_load {
|
||||
for dep_id in dependants_waiting_on_rec_load {
|
||||
Self::propagate_loaded_state(infos, waiting_id, dep_id, sender);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively propagates failed state up the dependency tree
|
||||
fn propagate_failed_state(
|
||||
infos: &mut AssetInfos,
|
||||
failed_id: UntypedAssetId,
|
||||
waiting_id: UntypedAssetId,
|
||||
) {
|
||||
let dependants_waiting_on_rec_load = if let Some(info) = infos.get_mut(waiting_id) {
|
||||
info.loading_rec_dependencies.remove(&failed_id);
|
||||
info.failed_rec_dependencies.insert(failed_id);
|
||||
info.rec_dep_load_state = RecursiveDependencyLoadState::Failed;
|
||||
Some(std::mem::take(
|
||||
&mut info.dependants_waiting_on_recursive_dep_load,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(dependants_waiting_on_rec_load) = dependants_waiting_on_rec_load {
|
||||
for dep_id in dependants_waiting_on_rec_load {
|
||||
Self::propagate_failed_state(infos, waiting_id, dep_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn process_asset_fail(&mut self, failed_id: UntypedAssetId) {
|
||||
let (dependants_waiting_on_load, dependants_waiting_on_rec_load) = {
|
||||
let info = self
|
||||
.get_mut(failed_id)
|
||||
.expect("Asset info should always exist at this point");
|
||||
info.load_state = LoadState::Failed;
|
||||
info.dep_load_state = DependencyLoadState::Failed;
|
||||
info.rec_dep_load_state = RecursiveDependencyLoadState::Failed;
|
||||
(
|
||||
std::mem::take(&mut info.dependants_waiting_on_load),
|
||||
std::mem::take(&mut info.dependants_waiting_on_recursive_dep_load),
|
||||
)
|
||||
};
|
||||
|
||||
for waiting_id in dependants_waiting_on_load {
|
||||
if let Some(info) = self.get_mut(waiting_id) {
|
||||
info.loading_dependencies.remove(&failed_id);
|
||||
info.failed_dependencies.insert(failed_id);
|
||||
info.dep_load_state = DependencyLoadState::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
for waiting_id in dependants_waiting_on_rec_load {
|
||||
Self::propagate_failed_state(self, failed_id, waiting_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn process_handle_drop_internal(
|
||||
infos: &mut HashMap<UntypedAssetId, AssetInfo>,
|
||||
path_to_id: &mut HashMap<AssetPath<'static>, UntypedAssetId>,
|
||||
loader_dependants: &mut HashMap<AssetPath<'static>, HashSet<AssetPath<'static>>>,
|
||||
watching_for_changes: bool,
|
||||
id: UntypedAssetId,
|
||||
) -> bool {
|
||||
match infos.entry(id) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
if entry.get_mut().handle_drops_to_skip > 0 {
|
||||
entry.get_mut().handle_drops_to_skip -= 1;
|
||||
false
|
||||
} else {
|
||||
let info = entry.remove();
|
||||
if let Some(path) = info.path {
|
||||
if watching_for_changes {
|
||||
for loader_dependency in info.loader_dependencies.keys() {
|
||||
if let Some(dependants) =
|
||||
loader_dependants.get_mut(loader_dependency)
|
||||
{
|
||||
dependants.remove(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
path_to_id.remove(&path);
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
// Either the asset was already dropped, it doesn't exist, or it isn't managed by the asset server
|
||||
// None of these cases should result in a removal from the Assets collection
|
||||
Entry::Vacant(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Consumes all current handle drop events. This will update information in [`AssetInfos`], but it
|
||||
/// will not affect [`Assets`] storages. For normal use cases, prefer `Assets::track_assets()`
|
||||
/// This should only be called if `Assets` storage isn't being used (such as in [`AssetProcessor`](crate::processor::AssetProcessor))
|
||||
///
|
||||
/// [`Assets`]: crate::Assets
|
||||
pub(crate) fn consume_handle_drop_events(&mut self) {
|
||||
for provider in self.handle_providers.values() {
|
||||
while let Ok(drop_event) = provider.drop_receiver.try_recv() {
|
||||
let id = drop_event.id;
|
||||
if drop_event.asset_server_managed {
|
||||
Self::process_handle_drop_internal(
|
||||
&mut self.infos,
|
||||
&mut self.path_to_id,
|
||||
&mut self.loader_dependants,
|
||||
self.watching_for_changes,
|
||||
id.untyped(provider.type_id),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines how a handle should be initialized
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum HandleLoadingMode {
|
||||
/// The handle is for an asset that isn't loading/loaded yet.
|
||||
NotLoading,
|
||||
/// The handle is for an asset that is being _requested_ to load (if it isn't already loading)
|
||||
Request,
|
||||
/// The handle is for an asset that is being forced to load (even if it has already loaded)
|
||||
Force,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("Cannot allocate a handle because no handle provider exists for asset type {0:?}")]
|
||||
pub struct MissingHandleProviderError(TypeId);
|
||||
|
||||
fn unwrap_with_context<T>(
|
||||
result: Result<T, MissingHandleProviderError>,
|
||||
type_name: &'static str,
|
||||
) -> T {
|
||||
match result {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
panic!("Cannot allocate an Asset Handle of type '{type_name}' because the asset type has not been initialized. \
|
||||
Make sure you have called app.init_asset::<{type_name}>()")
|
||||
}
|
||||
}
|
||||
}
|
913
crates/bevy_asset/src/server/mod.rs
Normal file
913
crates/bevy_asset/src/server/mod.rs
Normal file
|
@ -0,0 +1,913 @@
|
|||
mod info;
|
||||
|
||||
use crate::{
|
||||
folder::LoadedFolder,
|
||||
io::{AssetReader, AssetReaderError, AssetSourceEvent, AssetWatcher, Reader},
|
||||
loader::{AssetLoader, AssetLoaderError, ErasedAssetLoader, LoadContext, LoadedAsset},
|
||||
meta::{
|
||||
loader_settings_meta_transform, AssetActionMinimal, AssetMetaDyn, AssetMetaMinimal,
|
||||
MetaTransform, Settings,
|
||||
},
|
||||
path::AssetPath,
|
||||
Asset, AssetEvent, AssetHandleProvider, AssetId, Assets, DeserializeMetaError,
|
||||
ErasedLoadedAsset, Handle, UntypedAssetId, UntypedHandle,
|
||||
};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_log::{error, info, warn};
|
||||
use bevy_tasks::IoTaskPool;
|
||||
use bevy_utils::{HashMap, HashSet};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use futures_lite::StreamExt;
|
||||
use info::*;
|
||||
use parking_lot::RwLock;
|
||||
use std::{any::TypeId, path::Path, sync::Arc};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Loads and tracks the state of [`Asset`] values from a configured [`AssetReader`]. This can be used to kick off new asset loads and
|
||||
/// retrieve their current load states.
|
||||
///
|
||||
/// The general process to load an asset is:
|
||||
/// 1. Initialize a new [`Asset`] type with the [`AssetServer`] via [`AssetApp::init_asset`], which will internally call [`AssetServer::register_asset`]
|
||||
/// and set up related ECS [`Assets`] storage and systems.
|
||||
/// 2. Register one or more [`AssetLoader`]s for that asset with [`AssetApp::init_asset_loader`]
|
||||
/// 3. Add the asset to your asset folder (defaults to `assets`).
|
||||
/// 4. Call [`AssetServer::load`] with a path to your asset.
|
||||
///
|
||||
/// [`AssetServer`] can be cloned. It is backed by an [`Arc`] so clones will share state. Clones can be freely used in parallel.
|
||||
///
|
||||
/// [`AssetApp::init_asset`]: crate::AssetApp::init_asset
|
||||
/// [`AssetApp::init_asset_loader`]: crate::AssetApp::init_asset_loader
|
||||
#[derive(Resource, Clone)]
|
||||
pub struct AssetServer {
|
||||
pub(crate) data: Arc<AssetServerData>,
|
||||
}
|
||||
|
||||
/// Internal data used by [`AssetServer`]. This is intended to be used from within an [`Arc`].
|
||||
pub(crate) struct AssetServerData {
|
||||
pub(crate) infos: RwLock<AssetInfos>,
|
||||
pub(crate) loaders: Arc<RwLock<AssetLoaders>>,
|
||||
asset_event_sender: Sender<InternalAssetEvent>,
|
||||
asset_event_receiver: Receiver<InternalAssetEvent>,
|
||||
source_event_receiver: Receiver<AssetSourceEvent>,
|
||||
reader: Box<dyn AssetReader>,
|
||||
_watcher: Option<Box<dyn AssetWatcher>>,
|
||||
}
|
||||
|
||||
impl AssetServer {
|
||||
/// Create a new instance of [`AssetServer`]. If `watch_for_changes` is true, the [`AssetReader`] storage will watch for changes to
|
||||
/// asset sources and hot-reload them.
|
||||
pub fn new(reader: Box<dyn AssetReader>, watch_for_changes: bool) -> Self {
|
||||
Self::new_with_loaders(reader, Default::default(), watch_for_changes)
|
||||
}
|
||||
|
||||
pub(crate) fn new_with_loaders(
|
||||
reader: Box<dyn AssetReader>,
|
||||
loaders: Arc<RwLock<AssetLoaders>>,
|
||||
watch_for_changes: bool,
|
||||
) -> Self {
|
||||
let (asset_event_sender, asset_event_receiver) = crossbeam_channel::unbounded();
|
||||
let (source_event_sender, source_event_receiver) = crossbeam_channel::unbounded();
|
||||
let mut infos = AssetInfos::default();
|
||||
let watcher = if watch_for_changes {
|
||||
infos.watching_for_changes = true;
|
||||
let watcher = reader.watch_for_changes(source_event_sender);
|
||||
if watcher.is_none() {
|
||||
error!("{}", CANNOT_WATCH_ERROR_MESSAGE);
|
||||
}
|
||||
watcher
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self {
|
||||
data: Arc::new(AssetServerData {
|
||||
reader,
|
||||
_watcher: watcher,
|
||||
asset_event_sender,
|
||||
asset_event_receiver,
|
||||
source_event_receiver,
|
||||
loaders,
|
||||
infos: RwLock::new(infos),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the primary [`AssetReader`].
|
||||
pub fn reader(&self) -> &dyn AssetReader {
|
||||
&*self.data.reader
|
||||
}
|
||||
|
||||
/// Registers a new [`AssetLoader`]. [`AssetLoader`]s must be registered before they can be used.
|
||||
pub fn register_loader<L: AssetLoader>(&self, loader: L) {
|
||||
let mut loaders = self.data.loaders.write();
|
||||
let type_name = std::any::type_name::<L>();
|
||||
let loader = Arc::new(loader);
|
||||
let (loader_index, is_new) =
|
||||
if let Some(index) = loaders.preregistered_loaders.remove(type_name) {
|
||||
(index, false)
|
||||
} else {
|
||||
(loaders.values.len(), true)
|
||||
};
|
||||
for extension in loader.extensions() {
|
||||
loaders
|
||||
.extension_to_index
|
||||
.insert(extension.to_string(), loader_index);
|
||||
}
|
||||
|
||||
if is_new {
|
||||
loaders.type_name_to_index.insert(type_name, loader_index);
|
||||
loaders.values.push(MaybeAssetLoader::Ready(loader));
|
||||
} else {
|
||||
let maybe_loader = std::mem::replace(
|
||||
&mut loaders.values[loader_index],
|
||||
MaybeAssetLoader::Ready(loader.clone()),
|
||||
);
|
||||
match maybe_loader {
|
||||
MaybeAssetLoader::Ready(_) => unreachable!(),
|
||||
MaybeAssetLoader::Pending { sender, .. } => {
|
||||
IoTaskPool::get()
|
||||
.spawn(async move {
|
||||
let _ = sender.broadcast(loader).await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a new [`Asset`] type. [`Asset`] types must be registered before assets of that type can be loaded.
|
||||
pub fn register_asset<A: Asset>(&self, assets: &Assets<A>) {
|
||||
self.register_handle_provider(assets.get_handle_provider());
|
||||
fn sender<A: Asset>(world: &mut World, id: UntypedAssetId) {
|
||||
world
|
||||
.resource_mut::<Events<AssetEvent<A>>>()
|
||||
.send(AssetEvent::LoadedWithDependencies { id: id.typed() });
|
||||
}
|
||||
self.data
|
||||
.infos
|
||||
.write()
|
||||
.dependency_loaded_event_sender
|
||||
.insert(TypeId::of::<A>(), sender::<A>);
|
||||
}
|
||||
|
||||
pub(crate) fn register_handle_provider(&self, handle_provider: AssetHandleProvider) {
|
||||
let mut infos = self.data.infos.write();
|
||||
infos
|
||||
.handle_providers
|
||||
.insert(handle_provider.type_id, handle_provider);
|
||||
}
|
||||
|
||||
/// Returns the registered [`AssetLoader`] associated with the given extension, if it exists.
|
||||
pub async fn get_asset_loader_with_extension(
|
||||
&self,
|
||||
extension: &str,
|
||||
) -> Result<Arc<dyn ErasedAssetLoader>, MissingAssetLoaderForExtensionError> {
|
||||
let loader = {
|
||||
let loaders = self.data.loaders.read();
|
||||
let index = *loaders.extension_to_index.get(extension).ok_or_else(|| {
|
||||
MissingAssetLoaderForExtensionError {
|
||||
extensions: vec![extension.to_string()],
|
||||
}
|
||||
})?;
|
||||
loaders.values[index].clone()
|
||||
};
|
||||
|
||||
match loader {
|
||||
MaybeAssetLoader::Ready(loader) => Ok(loader),
|
||||
MaybeAssetLoader::Pending { mut receiver, .. } => Ok(receiver.recv().await.unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the registered [`AssetLoader`] associated with the given [`std::any::type_name`], if it exists.
|
||||
pub async fn get_asset_loader_with_type_name(
|
||||
&self,
|
||||
type_name: &str,
|
||||
) -> Result<Arc<dyn ErasedAssetLoader>, MissingAssetLoaderForTypeNameError> {
|
||||
let loader = {
|
||||
let loaders = self.data.loaders.read();
|
||||
let index = *loaders.type_name_to_index.get(type_name).ok_or_else(|| {
|
||||
MissingAssetLoaderForTypeNameError {
|
||||
type_name: type_name.to_string(),
|
||||
}
|
||||
})?;
|
||||
|
||||
loaders.values[index].clone()
|
||||
};
|
||||
match loader {
|
||||
MaybeAssetLoader::Ready(loader) => Ok(loader),
|
||||
MaybeAssetLoader::Pending { mut receiver, .. } => Ok(receiver.recv().await.unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the default [`AssetLoader`] for the given path, if one can be found.
|
||||
pub async fn get_path_asset_loader<'a>(
|
||||
&self,
|
||||
path: &AssetPath<'a>,
|
||||
) -> Result<Arc<dyn ErasedAssetLoader>, MissingAssetLoaderForExtensionError> {
|
||||
let full_extension =
|
||||
path.get_full_extension()
|
||||
.ok_or(MissingAssetLoaderForExtensionError {
|
||||
extensions: Vec::new(),
|
||||
})?;
|
||||
if let Ok(loader) = self.get_asset_loader_with_extension(&full_extension).await {
|
||||
return Ok(loader);
|
||||
}
|
||||
for extension in AssetPath::iter_secondary_extensions(&full_extension) {
|
||||
if let Ok(loader) = self.get_asset_loader_with_extension(extension).await {
|
||||
return Ok(loader);
|
||||
}
|
||||
}
|
||||
let mut extensions = vec![full_extension.clone()];
|
||||
extensions
|
||||
.extend(AssetPath::iter_secondary_extensions(&full_extension).map(|e| e.to_string()));
|
||||
Err(MissingAssetLoaderForExtensionError { extensions })
|
||||
}
|
||||
|
||||
/// Begins loading an [`Asset`] of type `A` stored at `path`. This will not block on the asset load. Instead,
|
||||
/// it returns a "strong" [`Handle`]. When the [`Asset`] is loaded (and enters [`LoadState::Loaded`]), it will be added to the
|
||||
/// associated [`Assets`] resource.
|
||||
///
|
||||
/// You can check the asset's load state by reading [`AssetEvent`] events, calling [`AssetServer::load_state`], or checking
|
||||
/// the [`Assets`] storage to see if the [`Asset`] exists yet.
|
||||
///
|
||||
/// The asset load will fail and an error will be printed to the logs if the asset stored at `path` is not of type `A`.
|
||||
#[must_use = "not using the returned strong handle may result in the unexpected release of the asset"]
|
||||
pub fn load<'a, A: Asset>(&self, path: impl Into<AssetPath<'a>>) -> Handle<A> {
|
||||
self.load_with_meta_transform(path, None)
|
||||
}
|
||||
|
||||
/// Begins loading an [`Asset`] of type `A` stored at `path`. The given `settings` function will override the asset's
|
||||
/// [`AssetLoader`] settings. The type `S` _must_ match the configured [`AssetLoader::Settings`] or `settings` changes
|
||||
/// will be ignored and an error will be printed to the log.
|
||||
#[must_use = "not using the returned strong handle may result in the unexpected release of the asset"]
|
||||
pub fn load_with_settings<'a, A: Asset, S: Settings>(
|
||||
&self,
|
||||
path: impl Into<AssetPath<'a>>,
|
||||
settings: impl Fn(&mut S) + Send + Sync + 'static,
|
||||
) -> Handle<A> {
|
||||
self.load_with_meta_transform(path, Some(loader_settings_meta_transform(settings)))
|
||||
}
|
||||
|
||||
fn load_with_meta_transform<'a, A: Asset>(
|
||||
&self,
|
||||
path: impl Into<AssetPath<'a>>,
|
||||
meta_transform: Option<MetaTransform>,
|
||||
) -> Handle<A> {
|
||||
let path: AssetPath = path.into();
|
||||
let (handle, should_load) = self.data.infos.write().get_or_create_path_handle::<A>(
|
||||
path.to_owned(),
|
||||
HandleLoadingMode::Request,
|
||||
meta_transform,
|
||||
);
|
||||
|
||||
if should_load {
|
||||
let mut owned_handle = Some(handle.clone().untyped());
|
||||
let mut owned_path = path.to_owned();
|
||||
let server = self.clone();
|
||||
IoTaskPool::get()
|
||||
.spawn(async move {
|
||||
if owned_path.label().is_some() {
|
||||
owned_path.remove_label();
|
||||
owned_handle = None;
|
||||
}
|
||||
if let Err(err) = server
|
||||
.load_internal(owned_handle, owned_path, false, None)
|
||||
.await
|
||||
{
|
||||
error!("{}", err);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
handle
|
||||
}
|
||||
|
||||
#[must_use = "not using the returned strong handle may result in the unexpected release of the asset"]
|
||||
pub(crate) async fn load_untyped_async<'a>(
|
||||
&self,
|
||||
path: impl Into<AssetPath<'a>>,
|
||||
) -> Result<UntypedHandle, AssetLoadError> {
|
||||
self.load_internal(None, path.into(), false, None).await
|
||||
}
|
||||
|
||||
async fn load_internal<'a>(
|
||||
&self,
|
||||
input_handle: Option<UntypedHandle>,
|
||||
mut path: AssetPath<'a>,
|
||||
force: bool,
|
||||
meta_transform: Option<MetaTransform>,
|
||||
) -> Result<UntypedHandle, AssetLoadError> {
|
||||
let owned_path = path.to_owned();
|
||||
let (mut meta, loader, mut reader) = self
|
||||
.get_meta_loader_and_reader(&owned_path)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
// if there was an input handle, a "load" operation has already started, so we must produce a "failure" event, if
|
||||
// we cannot find the meta and loader
|
||||
if let Some(handle) = &input_handle {
|
||||
self.send_asset_event(InternalAssetEvent::Failed { id: handle.id() });
|
||||
}
|
||||
e
|
||||
})?;
|
||||
|
||||
let has_label = path.label().is_some();
|
||||
|
||||
let (handle, should_load) = match input_handle {
|
||||
Some(handle) => {
|
||||
if !has_label && handle.type_id() != loader.asset_type_id() {
|
||||
return Err(AssetLoadError::RequestedHandleTypeMismatch {
|
||||
path: path.to_owned(),
|
||||
requested: handle.type_id(),
|
||||
actual_asset_name: loader.asset_type_name(),
|
||||
loader_name: loader.type_name(),
|
||||
});
|
||||
}
|
||||
// if a handle was passed in, the "should load" check was already done
|
||||
(handle, true)
|
||||
}
|
||||
None => {
|
||||
let mut infos = self.data.infos.write();
|
||||
infos.get_or_create_path_handle_untyped(
|
||||
path.to_owned(),
|
||||
loader.asset_type_id(),
|
||||
loader.asset_type_name(),
|
||||
HandleLoadingMode::Request,
|
||||
meta_transform,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if !should_load && !force {
|
||||
return Ok(handle);
|
||||
}
|
||||
let base_asset_id = if has_label {
|
||||
path.remove_label();
|
||||
// If the path has a label, the current id does not match the asset root type.
|
||||
// We need to get the actual asset id
|
||||
let mut infos = self.data.infos.write();
|
||||
let (actual_handle, _) = infos.get_or_create_path_handle_untyped(
|
||||
path.to_owned(),
|
||||
loader.asset_type_id(),
|
||||
loader.asset_type_name(),
|
||||
// ignore current load state ... we kicked off this sub asset load because it needed to be loaded but
|
||||
// does not currently exist
|
||||
HandleLoadingMode::Force,
|
||||
None,
|
||||
);
|
||||
actual_handle.id()
|
||||
} else {
|
||||
handle.id()
|
||||
};
|
||||
|
||||
if let Some(meta_transform) = handle.meta_transform() {
|
||||
(*meta_transform)(&mut *meta);
|
||||
}
|
||||
|
||||
match self
|
||||
.load_with_meta_loader_and_reader(&path, meta, &*loader, &mut *reader, true, false)
|
||||
.await
|
||||
{
|
||||
Ok(mut loaded_asset) => {
|
||||
for (_, labeled_asset) in loaded_asset.labeled_assets.drain() {
|
||||
self.send_asset_event(InternalAssetEvent::Loaded {
|
||||
id: labeled_asset.handle.id(),
|
||||
loaded_asset: labeled_asset.asset,
|
||||
});
|
||||
}
|
||||
self.send_asset_event(InternalAssetEvent::Loaded {
|
||||
id: base_asset_id,
|
||||
loaded_asset,
|
||||
});
|
||||
Ok(handle)
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_asset_event(InternalAssetEvent::Failed { id: base_asset_id });
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kicks off a reload of the asset stored at the given path. This will only reload the asset if it currently loaded.
|
||||
pub fn reload<'a>(&self, path: impl Into<AssetPath<'a>>) {
|
||||
let server = self.clone();
|
||||
let path = path.into();
|
||||
let owned_path = path.to_owned();
|
||||
IoTaskPool::get()
|
||||
.spawn(async move {
|
||||
if server.data.infos.read().is_path_alive(&owned_path) {
|
||||
info!("Reloading {owned_path} because it has changed");
|
||||
if let Err(err) = server.load_internal(None, owned_path, true, None).await {
|
||||
error!("{}", err);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Queues a new asset to be tracked by the [`AssetServer`] and returns a [`Handle`] to it. This can be used to track
|
||||
/// dependencies of assets created at runtime.
|
||||
///
|
||||
/// After the asset has been fully loaded by the [`AssetServer`], it will show up in the relevant [`Assets`] storage.
|
||||
#[must_use = "not using the returned strong handle may result in the unexpected release of the asset"]
|
||||
pub fn add<A: Asset>(&self, asset: A) -> Handle<A> {
|
||||
self.load_asset(LoadedAsset::new_with_dependencies(asset, None))
|
||||
}
|
||||
|
||||
pub(crate) fn load_asset<A: Asset>(&self, asset: impl Into<LoadedAsset<A>>) -> Handle<A> {
|
||||
let loaded_asset: LoadedAsset<A> = asset.into();
|
||||
let erased_loaded_asset: ErasedLoadedAsset = loaded_asset.into();
|
||||
self.load_asset_untyped(None, erased_loaded_asset)
|
||||
.typed_debug_checked()
|
||||
}
|
||||
|
||||
#[must_use = "not using the returned strong handle may result in the unexpected release of the asset"]
|
||||
pub(crate) fn load_asset_untyped(
|
||||
&self,
|
||||
path: Option<&AssetPath<'static>>,
|
||||
asset: impl Into<ErasedLoadedAsset>,
|
||||
) -> UntypedHandle {
|
||||
let loaded_asset = asset.into();
|
||||
let handle = if let Some(path) = path {
|
||||
let (handle, _) = self.data.infos.write().get_or_create_path_handle_untyped(
|
||||
path.clone(),
|
||||
loaded_asset.asset_type_id(),
|
||||
loaded_asset.asset_type_name(),
|
||||
HandleLoadingMode::NotLoading,
|
||||
None,
|
||||
);
|
||||
handle
|
||||
} else {
|
||||
self.data.infos.write().create_loading_handle_untyped(
|
||||
loaded_asset.asset_type_id(),
|
||||
loaded_asset.asset_type_name(),
|
||||
)
|
||||
};
|
||||
self.send_asset_event(InternalAssetEvent::Loaded {
|
||||
id: handle.id(),
|
||||
loaded_asset,
|
||||
});
|
||||
handle
|
||||
}
|
||||
|
||||
/// Loads all assets from the specified folder recursively. The [`LoadedFolder`] asset (when it loads) will
|
||||
/// contain handles to all assets in the folder. You can wait for all assets to load by checking the [`LoadedFolder`]'s
|
||||
/// [`RecursiveDependencyLoadState`].
|
||||
#[must_use = "not using the returned strong handle may result in the unexpected release of the assets"]
|
||||
pub fn load_folder(&self, path: impl AsRef<Path>) -> Handle<LoadedFolder> {
|
||||
let handle = {
|
||||
let mut infos = self.data.infos.write();
|
||||
infos.create_loading_handle::<LoadedFolder>()
|
||||
};
|
||||
let id = handle.id().untyped();
|
||||
|
||||
fn load_folder<'a>(
|
||||
path: &'a Path,
|
||||
server: &'a AssetServer,
|
||||
handles: &'a mut Vec<UntypedHandle>,
|
||||
) -> bevy_utils::BoxedFuture<'a, Result<(), AssetLoadError>> {
|
||||
Box::pin(async move {
|
||||
let is_dir = server.reader().is_directory(path).await?;
|
||||
if is_dir {
|
||||
let mut path_stream = server.reader().read_directory(path.as_ref()).await?;
|
||||
while let Some(child_path) = path_stream.next().await {
|
||||
if server.reader().is_directory(&child_path).await? {
|
||||
load_folder(&child_path, server, handles).await?;
|
||||
} else {
|
||||
let path = child_path.to_str().expect("Path should be a valid string.");
|
||||
match server.load_untyped_async(path).await {
|
||||
Ok(handle) => handles.push(handle),
|
||||
// skip assets that cannot be loaded
|
||||
Err(
|
||||
AssetLoadError::MissingAssetLoaderForTypeName(_)
|
||||
| AssetLoadError::MissingAssetLoaderForExtension(_),
|
||||
) => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
let server = self.clone();
|
||||
let owned_path = path.as_ref().to_owned();
|
||||
IoTaskPool::get()
|
||||
.spawn(async move {
|
||||
let mut handles = Vec::new();
|
||||
match load_folder(&owned_path, &server, &mut handles).await {
|
||||
Ok(_) => server.send_asset_event(InternalAssetEvent::Loaded {
|
||||
id,
|
||||
loaded_asset: LoadedAsset::new_with_dependencies(
|
||||
LoadedFolder { handles },
|
||||
None,
|
||||
)
|
||||
.into(),
|
||||
}),
|
||||
Err(_) => server.send_asset_event(InternalAssetEvent::Failed { id }),
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
handle
|
||||
}
|
||||
|
||||
fn send_asset_event(&self, event: InternalAssetEvent) {
|
||||
self.data.asset_event_sender.send(event).unwrap();
|
||||
}
|
||||
|
||||
/// Retrieves all loads states for the given asset id.
|
||||
pub fn get_load_states(
|
||||
&self,
|
||||
id: impl Into<UntypedAssetId>,
|
||||
) -> Option<(LoadState, DependencyLoadState, RecursiveDependencyLoadState)> {
|
||||
self.data
|
||||
.infos
|
||||
.read()
|
||||
.get(id.into())
|
||||
.map(|i| (i.load_state, i.dep_load_state, i.rec_dep_load_state))
|
||||
}
|
||||
|
||||
/// Retrieves the main [`LoadState`] of a given asset `id`.
|
||||
pub fn get_load_state(&self, id: impl Into<UntypedAssetId>) -> Option<LoadState> {
|
||||
self.data.infos.read().get(id.into()).map(|i| i.load_state)
|
||||
}
|
||||
|
||||
/// Retrieves the [`RecursiveDependencyLoadState`] of a given asset `id`.
|
||||
pub fn get_recursive_dependency_load_state(
|
||||
&self,
|
||||
id: impl Into<UntypedAssetId>,
|
||||
) -> Option<RecursiveDependencyLoadState> {
|
||||
self.data
|
||||
.infos
|
||||
.read()
|
||||
.get(id.into())
|
||||
.map(|i| i.rec_dep_load_state)
|
||||
}
|
||||
|
||||
/// Retrieves the main [`LoadState`] of a given asset `id`.
|
||||
pub fn load_state(&self, id: impl Into<UntypedAssetId>) -> LoadState {
|
||||
self.get_load_state(id).unwrap_or(LoadState::NotLoaded)
|
||||
}
|
||||
|
||||
/// Retrieves the [`RecursiveDependencyLoadState`] of a given asset `id`.
|
||||
pub fn recursive_dependency_load_state(
|
||||
&self,
|
||||
id: impl Into<UntypedAssetId>,
|
||||
) -> RecursiveDependencyLoadState {
|
||||
self.get_recursive_dependency_load_state(id)
|
||||
.unwrap_or(RecursiveDependencyLoadState::NotLoaded)
|
||||
}
|
||||
|
||||
/// Returns an active handle for the given path, if the asset at the given path has already started loading,
|
||||
/// or is still "alive".
|
||||
pub fn get_handle<'a, A: Asset>(&self, path: impl Into<AssetPath<'a>>) -> Option<Handle<A>> {
|
||||
self.get_handle_untyped(path)
|
||||
.map(|h| h.typed_debug_checked())
|
||||
}
|
||||
|
||||
pub fn get_id_handle<A: Asset>(&self, id: AssetId<A>) -> Option<Handle<A>> {
|
||||
self.get_id_handle_untyped(id.untyped()).map(|h| h.typed())
|
||||
}
|
||||
|
||||
pub fn get_id_handle_untyped(&self, id: UntypedAssetId) -> Option<UntypedHandle> {
|
||||
self.data.infos.read().get_id_handle(id)
|
||||
}
|
||||
|
||||
/// Returns an active untyped handle for the given path, if the asset at the given path has already started loading,
|
||||
/// or is still "alive".
|
||||
pub fn get_handle_untyped<'a>(&self, path: impl Into<AssetPath<'a>>) -> Option<UntypedHandle> {
|
||||
let infos = self.data.infos.read();
|
||||
let path = path.into();
|
||||
infos.get_path_handle(path)
|
||||
}
|
||||
|
||||
/// Returns the path for the given `id`, if it has one.
|
||||
pub fn get_path(&self, id: impl Into<UntypedAssetId>) -> Option<AssetPath<'static>> {
|
||||
let infos = self.data.infos.read();
|
||||
let info = infos.get(id.into())?;
|
||||
Some(info.path.as_ref()?.to_owned())
|
||||
}
|
||||
|
||||
/// Pre-register a loader that will later be added.
|
||||
///
|
||||
/// Assets loaded with matching extensions will be blocked until the
|
||||
/// real loader is added.
|
||||
pub fn preregister_loader<L: AssetLoader>(&self, extensions: &[&str]) {
|
||||
let mut loaders = self.data.loaders.write();
|
||||
let loader_index = loaders.values.len();
|
||||
let type_name = std::any::type_name::<L>();
|
||||
loaders
|
||||
.preregistered_loaders
|
||||
.insert(type_name, loader_index);
|
||||
loaders.type_name_to_index.insert(type_name, loader_index);
|
||||
for extension in extensions {
|
||||
if loaders
|
||||
.extension_to_index
|
||||
.insert(extension.to_string(), loader_index)
|
||||
.is_some()
|
||||
{
|
||||
warn!("duplicate preregistration for `{extension}`, any assets loaded with the previous loader will never complete.");
|
||||
}
|
||||
}
|
||||
let (mut sender, receiver) = async_broadcast::broadcast(1);
|
||||
sender.set_overflow(true);
|
||||
loaders
|
||||
.values
|
||||
.push(MaybeAssetLoader::Pending { sender, receiver });
|
||||
}
|
||||
|
||||
/// Retrieve a handle for the given path. This will create a handle (and [`AssetInfo`]) if it does not exist
|
||||
pub(crate) fn get_or_create_path_handle<A: Asset>(
|
||||
&self,
|
||||
path: AssetPath<'static>,
|
||||
meta_transform: Option<MetaTransform>,
|
||||
) -> Handle<A> {
|
||||
let mut infos = self.data.infos.write();
|
||||
infos
|
||||
.get_or_create_path_handle::<A>(path, HandleLoadingMode::NotLoading, meta_transform)
|
||||
.0
|
||||
}
|
||||
|
||||
pub(crate) async fn get_meta_loader_and_reader<'a>(
|
||||
&'a self,
|
||||
asset_path: &'a AssetPath<'_>,
|
||||
) -> Result<
|
||||
(
|
||||
Box<dyn AssetMetaDyn>,
|
||||
Arc<dyn ErasedAssetLoader>,
|
||||
Box<Reader<'a>>,
|
||||
),
|
||||
AssetLoadError,
|
||||
> {
|
||||
// NOTE: We grab the asset byte reader first to ensure this is transactional for AssetReaders like ProcessorGatedReader
|
||||
// The asset byte reader will "lock" the processed asset, preventing writes for the duration of the lock.
|
||||
// Then the meta reader, if meta exists, will correspond to the meta for the current "version" of the asset.
|
||||
// See ProcessedAssetInfo::file_transaction_lock for more context
|
||||
let reader = self.data.reader.read(asset_path.path()).await?;
|
||||
match self.data.reader.read_meta_bytes(asset_path.path()).await {
|
||||
Ok(meta_bytes) => {
|
||||
// TODO: this isn't fully minimal yet. we only need the loader
|
||||
let minimal: AssetMetaMinimal = ron::de::from_bytes(&meta_bytes).map_err(|e| {
|
||||
AssetLoadError::DeserializeMeta(DeserializeMetaError::DeserializeMinimal(e))
|
||||
})?;
|
||||
let loader_name = match minimal.asset {
|
||||
AssetActionMinimal::Load { loader } => loader,
|
||||
AssetActionMinimal::Process { .. } => {
|
||||
return Err(AssetLoadError::CannotLoadProcessedAsset {
|
||||
path: asset_path.to_owned(),
|
||||
})
|
||||
}
|
||||
AssetActionMinimal::Ignore => {
|
||||
return Err(AssetLoadError::CannotLoadIgnoredAsset {
|
||||
path: asset_path.to_owned(),
|
||||
})
|
||||
}
|
||||
};
|
||||
let loader = self.get_asset_loader_with_type_name(&loader_name).await?;
|
||||
let meta = loader.deserialize_meta(&meta_bytes).map_err(|e| {
|
||||
AssetLoadError::AssetLoaderError {
|
||||
path: asset_path.to_owned(),
|
||||
loader: loader.type_name(),
|
||||
error: AssetLoaderError::DeserializeMeta(e),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok((meta, loader, reader))
|
||||
}
|
||||
Err(AssetReaderError::NotFound(_)) => {
|
||||
let loader = self.get_path_asset_loader(asset_path).await?;
|
||||
let meta = loader.default_meta();
|
||||
Ok((meta, loader, reader))
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn load_with_meta_loader_and_reader(
|
||||
&self,
|
||||
asset_path: &AssetPath<'_>,
|
||||
meta: Box<dyn AssetMetaDyn>,
|
||||
loader: &dyn ErasedAssetLoader,
|
||||
reader: &mut Reader<'_>,
|
||||
load_dependencies: bool,
|
||||
populate_hashes: bool,
|
||||
) -> Result<ErasedLoadedAsset, AssetLoadError> {
|
||||
let load_context = LoadContext::new(
|
||||
self,
|
||||
asset_path.to_owned(),
|
||||
load_dependencies,
|
||||
populate_hashes,
|
||||
);
|
||||
loader.load(reader, meta, load_context).await.map_err(|e| {
|
||||
AssetLoadError::AssetLoaderError {
|
||||
loader: loader.type_name(),
|
||||
path: asset_path.to_owned(),
|
||||
error: e,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A system that manages internal [`AssetServer`] events, such as finalizing asset loads.
|
||||
pub fn handle_internal_asset_events(world: &mut World) {
|
||||
world.resource_scope(|world, server: Mut<AssetServer>| {
|
||||
let mut infos = server.data.infos.write();
|
||||
for event in server.data.asset_event_receiver.try_iter() {
|
||||
match event {
|
||||
InternalAssetEvent::Loaded { id, loaded_asset } => {
|
||||
infos.process_asset_load(
|
||||
id,
|
||||
loaded_asset,
|
||||
world,
|
||||
&server.data.asset_event_sender,
|
||||
);
|
||||
}
|
||||
InternalAssetEvent::LoadedWithDependencies { id } => {
|
||||
let sender = infos
|
||||
.dependency_loaded_event_sender
|
||||
.get(&id.type_id())
|
||||
.expect("Asset event sender should exist");
|
||||
sender(world, id);
|
||||
}
|
||||
InternalAssetEvent::Failed { id } => infos.process_asset_fail(id),
|
||||
}
|
||||
}
|
||||
|
||||
fn queue_ancestors(
|
||||
asset_path: &AssetPath,
|
||||
infos: &AssetInfos,
|
||||
paths_to_reload: &mut HashSet<AssetPath<'static>>,
|
||||
) {
|
||||
if let Some(dependants) = infos.loader_dependants.get(asset_path) {
|
||||
for dependant in dependants {
|
||||
paths_to_reload.insert(dependant.to_owned());
|
||||
queue_ancestors(dependant, infos, paths_to_reload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut paths_to_reload = HashSet::new();
|
||||
for event in server.data.source_event_receiver.try_iter() {
|
||||
match event {
|
||||
// TODO: if the asset was processed and the processed file was changed, the first modified event
|
||||
// should be skipped?
|
||||
AssetSourceEvent::ModifiedAsset(path) | AssetSourceEvent::ModifiedMeta(path) => {
|
||||
queue_ancestors(
|
||||
&AssetPath::new_ref(&path, None),
|
||||
&infos,
|
||||
&mut paths_to_reload,
|
||||
);
|
||||
paths_to_reload.insert(path.into());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for path in paths_to_reload {
|
||||
server.reload(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct AssetLoaders {
|
||||
values: Vec<MaybeAssetLoader>,
|
||||
extension_to_index: HashMap<String, usize>,
|
||||
type_name_to_index: HashMap<&'static str, usize>,
|
||||
preregistered_loaders: HashMap<&'static str, usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum MaybeAssetLoader {
|
||||
Ready(Arc<dyn ErasedAssetLoader>),
|
||||
Pending {
|
||||
sender: async_broadcast::Sender<Arc<dyn ErasedAssetLoader>>,
|
||||
receiver: async_broadcast::Receiver<Arc<dyn ErasedAssetLoader>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Internal events for asset load results
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum InternalAssetEvent {
|
||||
Loaded {
|
||||
id: UntypedAssetId,
|
||||
loaded_asset: ErasedLoadedAsset,
|
||||
},
|
||||
LoadedWithDependencies {
|
||||
id: UntypedAssetId,
|
||||
},
|
||||
Failed {
|
||||
id: UntypedAssetId,
|
||||
},
|
||||
}
|
||||
|
||||
/// The load state of an asset.
|
||||
#[derive(Component, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum LoadState {
|
||||
/// The asset has not started loading yet
|
||||
NotLoaded,
|
||||
/// The asset is in the process of loading.
|
||||
Loading,
|
||||
/// The asset has been loaded and has been added to the [`World`]
|
||||
Loaded,
|
||||
/// The asset failed to load.
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// The load state of an asset's dependencies.
|
||||
#[derive(Component, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum DependencyLoadState {
|
||||
/// The asset has not started loading yet
|
||||
NotLoaded,
|
||||
/// Dependencies are still loading
|
||||
Loading,
|
||||
/// Dependencies have all loaded
|
||||
Loaded,
|
||||
/// One or more dependencies have failed to load
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// The recursive load state of an asset's dependencies.
|
||||
#[derive(Component, Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub enum RecursiveDependencyLoadState {
|
||||
/// The asset has not started loading yet
|
||||
NotLoaded,
|
||||
/// Dependencies in this asset's dependency tree are still loading
|
||||
Loading,
|
||||
/// Dependencies in this asset's dependency tree have all loaded
|
||||
Loaded,
|
||||
/// One or more dependencies have failed to load in this asset's dependency tree
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// An error that occurs during an [`Asset`] load.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum AssetLoadError {
|
||||
#[error("Requested handle of type {requested:?} for asset '{path}' does not match actual asset type '{actual_asset_name}', which used loader '{loader_name}'")]
|
||||
RequestedHandleTypeMismatch {
|
||||
path: AssetPath<'static>,
|
||||
requested: TypeId,
|
||||
actual_asset_name: &'static str,
|
||||
loader_name: &'static str,
|
||||
},
|
||||
#[error(transparent)]
|
||||
MissingAssetLoaderForExtension(#[from] MissingAssetLoaderForExtensionError),
|
||||
#[error(transparent)]
|
||||
MissingAssetLoaderForTypeName(#[from] MissingAssetLoaderForTypeNameError),
|
||||
#[error(transparent)]
|
||||
AssetReaderError(#[from] AssetReaderError),
|
||||
#[error("Encountered an error while reading asset metadata bytes")]
|
||||
AssetMetaReadError,
|
||||
#[error(transparent)]
|
||||
DeserializeMeta(DeserializeMetaError),
|
||||
#[error("Asset '{path}' is configured to be processed. It cannot be loaded directly.")]
|
||||
CannotLoadProcessedAsset { path: AssetPath<'static> },
|
||||
#[error("Asset '{path}' is configured to be ignored. It cannot be loaded.")]
|
||||
CannotLoadIgnoredAsset { path: AssetPath<'static> },
|
||||
#[error("Asset '{path}' encountered an error in {loader}: {error}")]
|
||||
AssetLoaderError {
|
||||
path: AssetPath<'static>,
|
||||
loader: &'static str,
|
||||
error: AssetLoaderError,
|
||||
},
|
||||
}
|
||||
|
||||
/// An error that occurs when an [`AssetLoader`] is not registered for a given extension.
|
||||
#[derive(Error, Debug)]
|
||||
#[error("no `AssetLoader` found{}", format_missing_asset_ext(.extensions))]
|
||||
pub struct MissingAssetLoaderForExtensionError {
|
||||
extensions: Vec<String>,
|
||||
}
|
||||
|
||||
/// An error that occurs when an [`AssetLoader`] is not registered for a given [`std::any::type_name`].
|
||||
#[derive(Error, Debug)]
|
||||
#[error("no `AssetLoader` found with the name '{type_name}'")]
|
||||
pub struct MissingAssetLoaderForTypeNameError {
|
||||
type_name: String,
|
||||
}
|
||||
|
||||
fn format_missing_asset_ext(exts: &[String]) -> String {
|
||||
if !exts.is_empty() {
|
||||
format!(
|
||||
" for the following extension{}: {}",
|
||||
if exts.len() > 1 { "s" } else { "" },
|
||||
exts.join(", ")
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AssetServer {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AssetServer")
|
||||
.field("info", &self.data.infos.read())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) static CANNOT_WATCH_ERROR_MESSAGE: &str =
|
||||
"Cannot watch for changes because the current `AssetReader` does not support it. If you are using \
|
||||
the FileAssetReader (the default on desktop platforms), enabling the filesystem_watcher feature will \
|
||||
add this functionality.";
|
|
@ -20,7 +20,6 @@ bevy_derive = { path = "../bevy_derive", version = "0.12.0-dev" }
|
|||
bevy_utils = { path = "../bevy_utils", version = "0.12.0-dev" }
|
||||
|
||||
# other
|
||||
anyhow = "1.0.4"
|
||||
rodio = { version = "0.17", default-features = false }
|
||||
parking_lot = "0.12.1"
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use anyhow::Result;
|
||||
use bevy_asset::{Asset, AssetLoader, LoadContext, LoadedAsset};
|
||||
use bevy_reflect::{TypePath, TypeUuid};
|
||||
use bevy_asset::{
|
||||
anyhow::Error,
|
||||
io::{AsyncReadExt, Reader},
|
||||
Asset, AssetLoader, LoadContext,
|
||||
};
|
||||
use bevy_reflect::TypePath;
|
||||
use bevy_utils::BoxedFuture;
|
||||
use std::{io::Cursor, sync::Arc};
|
||||
|
||||
/// A source of audio data
|
||||
#[derive(Debug, Clone, TypeUuid, TypePath)]
|
||||
#[uuid = "7a14806a-672b-443b-8d16-4f18afefa463"]
|
||||
#[derive(Asset, Debug, Clone, TypePath)]
|
||||
pub struct AudioSource {
|
||||
/// Raw data of the audio source.
|
||||
///
|
||||
|
@ -38,11 +40,22 @@ impl AsRef<[u8]> for AudioSource {
|
|||
pub struct AudioLoader;
|
||||
|
||||
impl AssetLoader for AudioLoader {
|
||||
fn load(&self, bytes: &[u8], load_context: &mut LoadContext) -> BoxedFuture<Result<()>> {
|
||||
load_context.set_default_asset(LoadedAsset::new(AudioSource {
|
||||
bytes: bytes.into(),
|
||||
}));
|
||||
Box::pin(async move { Ok(()) })
|
||||
type Asset = AudioSource;
|
||||
type Settings = ();
|
||||
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
reader: &'a mut Reader,
|
||||
_settings: &'a Self::Settings,
|
||||
_load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, Result<AudioSource, Error>> {
|
||||
Box::pin(async move {
|
||||
let mut bytes = Vec::new();
|
||||
reader.read_to_end(&mut bytes).await?;
|
||||
Ok(AudioSource {
|
||||
bytes: bytes.into(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &[&str] {
|
||||
|
@ -64,8 +77,7 @@ impl AssetLoader for AudioLoader {
|
|||
}
|
||||
|
||||
/// A type implementing this trait can be converted to a [`rodio::Source`] type.
|
||||
/// It must be [`Send`] and [`Sync`], and usually implements [`Asset`] so needs to be [`TypeUuid`],
|
||||
/// in order to be registered.
|
||||
/// It must be [`Send`] and [`Sync`] in order to be registered.
|
||||
/// Types that implement this trait usually contain raw sound data that can be converted into an iterator of samples.
|
||||
/// This trait is implemented for [`AudioSource`].
|
||||
/// Check the example [`decodable`](https://github.com/bevyengine/bevy/blob/latest/examples/audio/decodable.rs) for how to implement this trait on a custom type.
|
||||
|
|
|
@ -50,7 +50,7 @@ pub use rodio::Sample;
|
|||
pub use sinks::*;
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{AddAsset, Asset};
|
||||
use bevy_asset::{Asset, AssetApp};
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
use audio_output::*;
|
||||
|
@ -90,7 +90,7 @@ impl AddAudioSource for App {
|
|||
T: Decodable + Asset,
|
||||
f32: rodio::cpal::FromSample<T::DecoderItem>,
|
||||
{
|
||||
self.add_asset::<T>().add_systems(
|
||||
self.init_asset::<T>().add_systems(
|
||||
PostUpdate,
|
||||
play_queued_audio_system::<T>.in_set(AudioPlaySet),
|
||||
);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use crate::{AudioSourceBundle, Decodable, SpatialAudioSourceBundle};
|
||||
use bevy_reflect::{TypePath, TypeUuid};
|
||||
use bevy_asset::Asset;
|
||||
use bevy_reflect::TypePath;
|
||||
use rodio::{source::SineWave, source::TakeDuration, Source};
|
||||
|
||||
/// A source of sine wave sound
|
||||
#[derive(Debug, Clone, TypeUuid, TypePath)]
|
||||
#[uuid = "cbc63be3-b0b9-4d2c-a03c-88b58f1a19ef"]
|
||||
#[derive(Asset, Debug, Clone, TypePath)]
|
||||
pub struct Pitch {
|
||||
/// Frequency at which sound will be played
|
||||
pub frequency: f32,
|
||||
|
|
|
@ -23,7 +23,7 @@ pub mod prelude {
|
|||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
|
||||
use bevy_utils::{Duration, HashSet, Instant};
|
||||
use bevy_utils::{Duration, HashSet, Instant, Uuid};
|
||||
use std::borrow::Cow;
|
||||
use std::ffi::OsString;
|
||||
use std::marker::PhantomData;
|
||||
|
@ -61,7 +61,8 @@ fn register_rust_types(app: &mut App) {
|
|||
.register_type::<Cow<'static, str>>()
|
||||
.register_type::<Cow<'static, Path>>()
|
||||
.register_type::<Duration>()
|
||||
.register_type::<Instant>();
|
||||
.register_type::<Instant>()
|
||||
.register_type::<Uuid>();
|
||||
}
|
||||
|
||||
fn register_math_types(app: &mut App) {
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{load_internal_asset, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_render::{render_resource::*, renderer::RenderDevice, RenderApp};
|
||||
|
||||
use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state;
|
||||
|
||||
pub const BLIT_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2312396983770133547);
|
||||
pub const BLIT_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(2312396983770133547);
|
||||
|
||||
/// Adds support for specialized "blit pipelines", which can be used to write one texture to another.
|
||||
pub struct BlitPlugin;
|
||||
|
@ -86,7 +84,7 @@ impl SpecializedRenderPipeline for BlitPipeline {
|
|||
layout: vec![self.texture_bind_group.clone()],
|
||||
vertex: fullscreen_shader_vertex_state(),
|
||||
fragment: Some(FragmentState {
|
||||
shader: BLIT_SHADER_HANDLE.typed(),
|
||||
shader: BLIT_SHADER_HANDLE,
|
||||
shader_defs: vec![],
|
||||
entry_point: "fs_main".into(),
|
||||
targets: vec![Some(ColorTargetState {
|
||||
|
|
|
@ -130,7 +130,7 @@ impl SpecializedRenderPipeline for BloomDownsamplingPipeline {
|
|||
layout,
|
||||
vertex: fullscreen_shader_vertex_state(),
|
||||
fragment: Some(FragmentState {
|
||||
shader: BLOOM_SHADER_HANDLE.typed::<Shader>(),
|
||||
shader: BLOOM_SHADER_HANDLE,
|
||||
shader_defs,
|
||||
entry_point,
|
||||
targets: vec![Some(ColorTargetState {
|
||||
|
|
|
@ -9,10 +9,9 @@ use crate::{
|
|||
core_3d::{self, CORE_3D},
|
||||
};
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{load_internal_asset, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_ecs::{prelude::*, query::QueryItem};
|
||||
use bevy_math::UVec2;
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_render::{
|
||||
camera::ExtractedCamera,
|
||||
extract_component::{
|
||||
|
@ -34,8 +33,7 @@ use upsampling_pipeline::{
|
|||
prepare_upsampling_pipeline, BloomUpsamplingPipeline, UpsamplingPipelineIds,
|
||||
};
|
||||
|
||||
const BLOOM_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 929599476923908);
|
||||
const BLOOM_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(929599476923908);
|
||||
|
||||
const BLOOM_TEXTURE_FORMAT: TextureFormat = TextureFormat::Rg11b10Float;
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ impl SpecializedRenderPipeline for BloomUpsamplingPipeline {
|
|||
layout: vec![self.bind_group_layout.clone()],
|
||||
vertex: fullscreen_shader_vertex_state(),
|
||||
fragment: Some(FragmentState {
|
||||
shader: BLOOM_SHADER_HANDLE.typed::<Shader>(),
|
||||
shader: BLOOM_SHADER_HANDLE,
|
||||
shader_defs: vec![],
|
||||
entry_point: "upsample".into(),
|
||||
targets: vec![Some(ColorTargetState {
|
||||
|
|
|
@ -4,9 +4,9 @@ use crate::{
|
|||
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
|
||||
};
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{load_internal_asset, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_ecs::{prelude::*, query::QueryItem};
|
||||
use bevy_reflect::{Reflect, TypeUuid};
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_render::{
|
||||
extract_component::{ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin},
|
||||
prelude::Camera,
|
||||
|
@ -92,8 +92,8 @@ impl ExtractComponent for ContrastAdaptiveSharpeningSettings {
|
|||
}
|
||||
}
|
||||
|
||||
const CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 6925381244141981602);
|
||||
const CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE: Handle<Shader> =
|
||||
Handle::weak_from_u128(6925381244141981602);
|
||||
|
||||
/// Adds Support for Contrast Adaptive Sharpening (CAS).
|
||||
pub struct CASPlugin;
|
||||
|
@ -231,7 +231,7 @@ impl SpecializedRenderPipeline for CASPipeline {
|
|||
layout: vec![self.texture_bind_group.clone()],
|
||||
vertex: fullscreen_shader_vertex_state(),
|
||||
fragment: Some(FragmentState {
|
||||
shader: CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE.typed(),
|
||||
shader: CONTRAST_ADAPTIVE_SHARPENING_SHADER_HANDLE,
|
||||
shader_defs,
|
||||
entry_point: "fragment".into(),
|
||||
targets: vec![Some(ColorTargetState {
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
use bevy_asset::HandleUntyped;
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_asset::Handle;
|
||||
use bevy_render::{prelude::Shader, render_resource::VertexState};
|
||||
|
||||
pub const FULLSCREEN_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 7837534426033940724);
|
||||
pub const FULLSCREEN_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(7837534426033940724);
|
||||
|
||||
/// uses the [`FULLSCREEN_SHADER_HANDLE`] to output a
|
||||
/// ```wgsl
|
||||
|
@ -18,7 +16,7 @@ pub const FULLSCREEN_SHADER_HANDLE: HandleUntyped =
|
|||
/// The draw call should render one triangle: `render_pass.draw(0..3, 0..1);`
|
||||
pub fn fullscreen_shader_vertex_state() -> VertexState {
|
||||
VertexState {
|
||||
shader: FULLSCREEN_SHADER_HANDLE.typed(),
|
||||
shader: FULLSCREEN_SHADER_HANDLE,
|
||||
shader_defs: Vec::new(),
|
||||
entry_point: "fullscreen_vertex_shader".into(),
|
||||
buffers: Vec::new(),
|
||||
|
|
|
@ -4,10 +4,10 @@ use crate::{
|
|||
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
|
||||
};
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{load_internal_asset, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_derive::Deref;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypeUuid};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||
use bevy_render::{
|
||||
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
||||
prelude::Camera,
|
||||
|
@ -75,8 +75,7 @@ impl Default for Fxaa {
|
|||
}
|
||||
}
|
||||
|
||||
const FXAA_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 4182761465141723543);
|
||||
const FXAA_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(4182761465141723543);
|
||||
|
||||
/// Adds support for Fast Approximate Anti-Aliasing (FXAA)
|
||||
pub struct FxaaPlugin;
|
||||
|
@ -179,7 +178,7 @@ impl SpecializedRenderPipeline for FxaaPipeline {
|
|||
layout: vec![self.texture_bind_group.clone()],
|
||||
vertex: fullscreen_shader_vertex_state(),
|
||||
fragment: Some(FragmentState {
|
||||
shader: FXAA_SHADER_HANDLE.typed(),
|
||||
shader: FXAA_SHADER_HANDLE,
|
||||
shader_defs: vec![
|
||||
format!("EDGE_THRESH_{}", key.edge_threshold.get_str()).into(),
|
||||
format!("EDGE_THRESH_MIN_{}", key.edge_threshold_min.get_str()).into(),
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{load_internal_asset, Handle, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_ecs::{
|
||||
prelude::{Component, Entity},
|
||||
query::With,
|
||||
schedule::IntoSystemConfigs,
|
||||
system::{Commands, Query, Res, ResMut, Resource},
|
||||
};
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_render::{
|
||||
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
||||
render_asset::RenderAssets,
|
||||
|
@ -25,8 +24,7 @@ use bevy_render::{
|
|||
Render, RenderApp, RenderSet,
|
||||
};
|
||||
|
||||
const SKYBOX_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 55594763423201);
|
||||
const SKYBOX_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(55594763423201);
|
||||
|
||||
pub struct SkyboxPlugin;
|
||||
|
||||
|
@ -134,7 +132,7 @@ impl SpecializedRenderPipeline for SkyboxPipeline {
|
|||
layout: vec![self.bind_group_layout.clone()],
|
||||
push_constant_ranges: Vec::new(),
|
||||
vertex: VertexState {
|
||||
shader: SKYBOX_SHADER_HANDLE.typed(),
|
||||
shader: SKYBOX_SHADER_HANDLE,
|
||||
shader_defs: Vec::new(),
|
||||
entry_point: "skybox_vertex".into(),
|
||||
buffers: Vec::new(),
|
||||
|
@ -162,7 +160,7 @@ impl SpecializedRenderPipeline for SkyboxPipeline {
|
|||
alpha_to_coverage_enabled: false,
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
shader: SKYBOX_SHADER_HANDLE.typed(),
|
||||
shader: SKYBOX_SHADER_HANDLE,
|
||||
shader_defs: Vec::new(),
|
||||
entry_point: "skybox_fragment".into(),
|
||||
targets: vec![Some(ColorTargetState {
|
||||
|
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
prepass::{DepthPrepass, MotionVectorPrepass, ViewPrepassTextures},
|
||||
};
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{load_internal_asset, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_core::FrameCount;
|
||||
use bevy_ecs::{
|
||||
prelude::{Bundle, Component, Entity},
|
||||
|
@ -15,7 +15,7 @@ use bevy_ecs::{
|
|||
world::{FromWorld, World},
|
||||
};
|
||||
use bevy_math::vec2;
|
||||
use bevy_reflect::{Reflect, TypeUuid};
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_render::{
|
||||
camera::{ExtractedCamera, MipBias, TemporalJitter},
|
||||
prelude::{Camera, Projection},
|
||||
|
@ -42,8 +42,7 @@ mod draw_3d_graph {
|
|||
}
|
||||
}
|
||||
|
||||
const TAA_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 656865235226276);
|
||||
const TAA_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(656865235226276);
|
||||
|
||||
/// Plugin for temporal anti-aliasing. Disables multisample anti-aliasing (MSAA).
|
||||
///
|
||||
|
@ -392,7 +391,7 @@ impl SpecializedRenderPipeline for TAAPipeline {
|
|||
layout: vec![self.taa_bind_group_layout.clone()],
|
||||
vertex: fullscreen_shader_vertex_state(),
|
||||
fragment: Some(FragmentState {
|
||||
shader: TAA_SHADER_HANDLE.typed::<Shader>(),
|
||||
shader: TAA_SHADER_HANDLE,
|
||||
shader_defs,
|
||||
entry_point: "taa".into(),
|
||||
targets: vec![
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state;
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Assets, Handle};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_reflect::{Reflect, TypeUuid};
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_render::camera::Camera;
|
||||
use bevy_render::extract_component::{ExtractComponent, ExtractComponentPlugin};
|
||||
use bevy_render::extract_resource::{ExtractResource, ExtractResourcePlugin};
|
||||
|
@ -17,11 +17,10 @@ mod node;
|
|||
use bevy_utils::default;
|
||||
pub use node::TonemappingNode;
|
||||
|
||||
const TONEMAPPING_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 17015368199668024512);
|
||||
const TONEMAPPING_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(17015368199668024512);
|
||||
|
||||
const TONEMAPPING_SHARED_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2499430578245347910);
|
||||
const TONEMAPPING_SHARED_SHADER_HANDLE: Handle<Shader> =
|
||||
Handle::weak_from_u128(2499430578245347910);
|
||||
|
||||
/// 3D LUT (look up table) textures used for tonemapping
|
||||
#[derive(Resource, Clone, ExtractResource)]
|
||||
|
@ -207,7 +206,7 @@ impl SpecializedRenderPipeline for TonemappingPipeline {
|
|||
layout: vec![self.texture_bind_group.clone()],
|
||||
vertex: fullscreen_shader_vertex_state(),
|
||||
fragment: Some(FragmentState {
|
||||
shader: TONEMAPPING_SHADER_HANDLE.typed(),
|
||||
shader: TONEMAPPING_SHADER_HANDLE,
|
||||
shader_defs,
|
||||
entry_point: "fragment".into(),
|
||||
targets: vec![Some(ColorTargetState {
|
||||
|
|
|
@ -16,10 +16,21 @@
|
|||
//!
|
||||
//! See the documentation on [`Gizmos`](crate::gizmos::Gizmos) for more examples.
|
||||
|
||||
use std::mem;
|
||||
pub mod gizmos;
|
||||
|
||||
#[cfg(feature = "bevy_sprite")]
|
||||
mod pipeline_2d;
|
||||
#[cfg(feature = "bevy_pbr")]
|
||||
mod pipeline_3d;
|
||||
|
||||
/// The `bevy_gizmos` prelude.
|
||||
pub mod prelude {
|
||||
#[doc(hidden)]
|
||||
pub use crate::{gizmos::Gizmos, AabbGizmo, AabbGizmoConfig, GizmoConfig};
|
||||
}
|
||||
|
||||
use bevy_app::{Last, Plugin, PostUpdate};
|
||||
use bevy_asset::{load_internal_asset, AddAsset, Assets, Handle, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Asset, AssetApp, Assets, Handle};
|
||||
use bevy_core::cast_slice;
|
||||
use bevy_ecs::{
|
||||
change_detection::DetectChanges,
|
||||
|
@ -33,7 +44,7 @@ use bevy_ecs::{
|
|||
Commands, Query, Res, ResMut, Resource, SystemParamItem,
|
||||
},
|
||||
};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath, TypeUuid};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath};
|
||||
use bevy_render::{
|
||||
color::Color,
|
||||
extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin},
|
||||
|
@ -54,24 +65,10 @@ use bevy_transform::{
|
|||
components::{GlobalTransform, Transform},
|
||||
TransformSystem,
|
||||
};
|
||||
|
||||
pub mod gizmos;
|
||||
|
||||
#[cfg(feature = "bevy_sprite")]
|
||||
mod pipeline_2d;
|
||||
#[cfg(feature = "bevy_pbr")]
|
||||
mod pipeline_3d;
|
||||
|
||||
use gizmos::{GizmoStorage, Gizmos};
|
||||
use std::mem;
|
||||
|
||||
/// The `bevy_gizmos` prelude.
|
||||
pub mod prelude {
|
||||
#[doc(hidden)]
|
||||
pub use crate::{gizmos::Gizmos, AabbGizmo, AabbGizmoConfig, GizmoConfig};
|
||||
}
|
||||
|
||||
const LINE_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 7414812689238026784);
|
||||
const LINE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(7414812689238026784);
|
||||
|
||||
/// A [`Plugin`] that provides an immediate mode drawing api for visual debugging.
|
||||
pub struct GizmoPlugin;
|
||||
|
@ -81,7 +78,7 @@ impl Plugin for GizmoPlugin {
|
|||
load_internal_asset!(app, LINE_SHADER_HANDLE, "lines.wgsl", Shader::from_wgsl);
|
||||
|
||||
app.add_plugins(UniformComponentPlugin::<LineGizmoUniform>::default())
|
||||
.add_asset::<LineGizmo>()
|
||||
.init_asset::<LineGizmo>()
|
||||
.add_plugins(RenderAssetPlugin::<LineGizmo>::default())
|
||||
.init_resource::<LineGizmoHandles>()
|
||||
.init_resource::<GizmoConfig>()
|
||||
|
@ -351,8 +348,7 @@ struct LineGizmoUniform {
|
|||
_padding: bevy_math::Vec2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, TypeUuid, TypePath)]
|
||||
#[uuid = "02b99cbf-bb26-4713-829a-aee8e08dedc0"]
|
||||
#[derive(Asset, Debug, Default, Clone, TypePath)]
|
||||
struct LineGizmo {
|
||||
positions: Vec<[f32; 3]>,
|
||||
colors: Vec<[f32; 4]>,
|
||||
|
|
|
@ -97,13 +97,13 @@ impl SpecializedRenderPipeline for LineGizmoPipeline {
|
|||
|
||||
RenderPipelineDescriptor {
|
||||
vertex: VertexState {
|
||||
shader: LINE_SHADER_HANDLE.typed(),
|
||||
shader: LINE_SHADER_HANDLE,
|
||||
entry_point: "vertex".into(),
|
||||
shader_defs: shader_defs.clone(),
|
||||
buffers: line_gizmo_vertex_buffer_layouts(key.strip),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
shader: LINE_SHADER_HANDLE.typed(),
|
||||
shader: LINE_SHADER_HANDLE,
|
||||
shader_defs,
|
||||
entry_point: "fragment".into(),
|
||||
targets: vec![Some(ColorTargetState {
|
||||
|
|
|
@ -103,13 +103,13 @@ impl SpecializedRenderPipeline for LineGizmoPipeline {
|
|||
|
||||
RenderPipelineDescriptor {
|
||||
vertex: VertexState {
|
||||
shader: LINE_SHADER_HANDLE.typed(),
|
||||
shader: LINE_SHADER_HANDLE,
|
||||
entry_point: "vertex".into(),
|
||||
shader_defs: shader_defs.clone(),
|
||||
buffers: line_gizmo_vertex_buffer_layouts(key.strip),
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
shader: LINE_SHADER_HANDLE.typed(),
|
||||
shader: LINE_SHADER_HANDLE,
|
||||
shader_defs,
|
||||
entry_point: "fragment".into(),
|
||||
targets: vec![Some(ColorTargetState {
|
||||
|
|
|
@ -37,7 +37,6 @@ gltf = { version = "1.3.0", default-features = false, features = [
|
|||
"utils",
|
||||
] }
|
||||
thiserror = "1.0"
|
||||
anyhow = "1.0.4"
|
||||
base64 = "0.13.0"
|
||||
percent-encoding = "2.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
|
|
@ -9,10 +9,10 @@ mod vertex_attributes;
|
|||
pub use loader::*;
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{AddAsset, Handle};
|
||||
use bevy_asset::{Asset, AssetApp, Handle};
|
||||
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
|
||||
use bevy_pbr::StandardMaterial;
|
||||
use bevy_reflect::{Reflect, TypePath, TypeUuid};
|
||||
use bevy_reflect::{Reflect, TypePath};
|
||||
use bevy_render::{
|
||||
mesh::{Mesh, MeshVertexAttribute},
|
||||
renderer::RenderDevice,
|
||||
|
@ -41,11 +41,11 @@ impl GltfPlugin {
|
|||
impl Plugin for GltfPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<GltfExtras>()
|
||||
.add_asset::<Gltf>()
|
||||
.add_asset::<GltfNode>()
|
||||
.add_asset::<GltfPrimitive>()
|
||||
.add_asset::<GltfMesh>()
|
||||
.preregister_asset_loader(&["gltf", "glb"]);
|
||||
.init_asset::<Gltf>()
|
||||
.init_asset::<GltfNode>()
|
||||
.init_asset::<GltfPrimitive>()
|
||||
.init_asset::<GltfMesh>()
|
||||
.preregister_asset_loader::<GltfLoader>(&["gltf", "glb"]);
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
|
@ -54,17 +54,15 @@ impl Plugin for GltfPlugin {
|
|||
|
||||
None => CompressedImageFormats::NONE,
|
||||
};
|
||||
app.add_asset_loader::<GltfLoader>(GltfLoader {
|
||||
app.register_asset_loader(GltfLoader {
|
||||
supported_compressed_formats,
|
||||
custom_vertex_attributes: self.custom_vertex_attributes.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Representation of a loaded glTF file
|
||||
/// (file loaded via the `AssetServer` with the extension `.glb` or `.gltf`).
|
||||
#[derive(Debug, TypeUuid, TypePath)]
|
||||
#[uuid = "5c7d5f8a-f7b0-4e45-a09e-406c0372fea2"]
|
||||
/// Representation of a loaded glTF file.
|
||||
#[derive(Asset, Debug, TypePath)]
|
||||
pub struct Gltf {
|
||||
pub scenes: Vec<Handle<Scene>>,
|
||||
pub named_scenes: HashMap<String, Handle<Scene>>,
|
||||
|
@ -83,8 +81,7 @@ pub struct Gltf {
|
|||
|
||||
/// A glTF node with all of its child nodes, its [`GltfMesh`],
|
||||
/// [`Transform`](bevy_transform::prelude::Transform) and an optional [`GltfExtras`].
|
||||
#[derive(Debug, Clone, TypeUuid, TypePath)]
|
||||
#[uuid = "dad74750-1fd6-460f-ac51-0a7937563865"]
|
||||
#[derive(Asset, Debug, Clone, TypePath)]
|
||||
pub struct GltfNode {
|
||||
pub children: Vec<GltfNode>,
|
||||
pub mesh: Option<Handle<GltfMesh>>,
|
||||
|
@ -94,16 +91,14 @@ pub struct GltfNode {
|
|||
|
||||
/// A glTF mesh, which may consist of multiple [`GltfPrimitives`](GltfPrimitive)
|
||||
/// and an optional [`GltfExtras`].
|
||||
#[derive(Debug, Clone, TypeUuid, TypePath)]
|
||||
#[uuid = "8ceaec9a-926a-4f29-8ee3-578a69f42315"]
|
||||
#[derive(Asset, Debug, Clone, TypePath)]
|
||||
pub struct GltfMesh {
|
||||
pub primitives: Vec<GltfPrimitive>,
|
||||
pub extras: Option<GltfExtras>,
|
||||
}
|
||||
|
||||
/// Part of a [`GltfMesh`] that consists of a [`Mesh`], an optional [`StandardMaterial`] and [`GltfExtras`].
|
||||
#[derive(Debug, Clone, TypeUuid, TypePath)]
|
||||
#[uuid = "cbfca302-82fd-41cb-af77-cab6b3d50af1"]
|
||||
#[derive(Asset, Debug, Clone, TypePath)]
|
||||
pub struct GltfPrimitive {
|
||||
pub mesh: Handle<Mesh>,
|
||||
pub material: Option<Handle<StandardMaterial>>,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{vertex_attributes::*, Gltf, GltfExtras, GltfNode};
|
||||
use anyhow::Result;
|
||||
use crate::{vertex_attributes::convert_attribute, Gltf, GltfExtras, GltfNode};
|
||||
use bevy_asset::{
|
||||
AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, HandleId, LoadContext, LoadedAsset,
|
||||
anyhow, io::Reader, AssetLoadError, AssetLoader, AsyncReadExt, Handle, LoadContext,
|
||||
ReadAssetBytesError,
|
||||
};
|
||||
use bevy_core::Name;
|
||||
use bevy_core_pipeline::prelude::Camera3dBundle;
|
||||
|
@ -24,7 +24,9 @@ use bevy_render::{
|
|||
prelude::SpatialBundle,
|
||||
primitives::Aabb,
|
||||
render_resource::{AddressMode, Face, FilterMode, PrimitiveTopology, SamplerDescriptor},
|
||||
texture::{CompressedImageFormats, Image, ImageSampler, ImageType, TextureError},
|
||||
texture::{
|
||||
CompressedImageFormats, Image, ImageLoaderSettings, ImageSampler, ImageType, TextureError,
|
||||
},
|
||||
};
|
||||
use bevy_scene::Scene;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
@ -38,7 +40,10 @@ use gltf::{
|
|||
Material, Node, Primitive,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{collections::VecDeque, path::Path};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
/// An error that occurs when loading a glTF file.
|
||||
|
@ -58,8 +63,10 @@ pub enum GltfError {
|
|||
InvalidImageMimeType(String),
|
||||
#[error("You may need to add the feature for the file format: {0}")]
|
||||
ImageError(#[from] TextureError),
|
||||
#[error("failed to load an asset path: {0}")]
|
||||
AssetIoError(#[from] AssetIoError),
|
||||
#[error("failed to read bytes from an asset path: {0}")]
|
||||
ReadAssetBytesError(#[from] ReadAssetBytesError),
|
||||
#[error("failed to load asset from an asset path: {0}")]
|
||||
AssetLoadError(#[from] AssetLoadError),
|
||||
#[error("Missing sampler for animation {0}")]
|
||||
MissingAnimationSampler(usize),
|
||||
#[error("failed to generate tangents: {0}")]
|
||||
|
@ -75,12 +82,19 @@ pub struct GltfLoader {
|
|||
}
|
||||
|
||||
impl AssetLoader for GltfLoader {
|
||||
type Asset = Gltf;
|
||||
type Settings = ();
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
bytes: &'a [u8],
|
||||
reader: &'a mut Reader,
|
||||
_settings: &'a (),
|
||||
load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, Result<()>> {
|
||||
Box::pin(async move { Ok(load_gltf(bytes, load_context, self).await?) })
|
||||
) -> bevy_utils::BoxedFuture<'a, Result<Gltf, anyhow::Error>> {
|
||||
Box::pin(async move {
|
||||
let mut bytes = Vec::new();
|
||||
reader.read_to_end(&mut bytes).await?;
|
||||
Ok(load_gltf(self, &bytes, load_context).await?)
|
||||
})
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &[&str] {
|
||||
|
@ -89,23 +103,17 @@ impl AssetLoader for GltfLoader {
|
|||
}
|
||||
|
||||
/// Loads an entire glTF file.
|
||||
async fn load_gltf<'a, 'b>(
|
||||
bytes: &'a [u8],
|
||||
load_context: &'a mut LoadContext<'b>,
|
||||
async fn load_gltf<'a, 'b, 'c>(
|
||||
loader: &GltfLoader,
|
||||
) -> Result<(), GltfError> {
|
||||
bytes: &'a [u8],
|
||||
load_context: &'b mut LoadContext<'c>,
|
||||
) -> Result<Gltf, GltfError> {
|
||||
let gltf = gltf::Gltf::from_slice(bytes)?;
|
||||
let buffer_data = load_buffers(&gltf, load_context, load_context.path()).await?;
|
||||
let buffer_data = load_buffers(&gltf, load_context).await?;
|
||||
|
||||
let mut materials = vec![];
|
||||
let mut named_materials = HashMap::default();
|
||||
let mut linear_textures = HashSet::default();
|
||||
|
||||
for material in gltf.materials() {
|
||||
let handle = load_material(&material, load_context);
|
||||
if let Some(name) = material.name() {
|
||||
named_materials.insert(name.to_string(), handle.clone());
|
||||
}
|
||||
materials.push(handle);
|
||||
if let Some(texture) = material.normal_texture() {
|
||||
linear_textures.insert(texture.texture().index());
|
||||
}
|
||||
|
@ -202,10 +210,8 @@ async fn load_gltf<'a, 'b>(
|
|||
);
|
||||
}
|
||||
}
|
||||
let handle = load_context.set_labeled_asset(
|
||||
&format!("Animation{}", animation.index()),
|
||||
LoadedAsset::new(animation_clip),
|
||||
);
|
||||
let handle = load_context
|
||||
.add_labeled_asset(format!("Animation{}", animation.index()), animation_clip);
|
||||
if let Some(name) = animation.name() {
|
||||
named_animations.insert(name.to_string(), handle.clone());
|
||||
}
|
||||
|
@ -214,6 +220,89 @@ async fn load_gltf<'a, 'b>(
|
|||
(animations, named_animations, animation_roots)
|
||||
};
|
||||
|
||||
// TODO: use the threaded impl on wasm once wasm thread pool doesn't deadlock on it
|
||||
// See https://github.com/bevyengine/bevy/issues/1924 for more details
|
||||
// The taskpool use is also avoided when there is only one texture for performance reasons and
|
||||
// to avoid https://github.com/bevyengine/bevy/pull/2725
|
||||
// PERF: could this be a Vec instead? Are gltf texture indices dense?
|
||||
fn process_loaded_texture(
|
||||
load_context: &mut LoadContext,
|
||||
handles: &mut Vec<Handle<Image>>,
|
||||
texture: ImageOrPath,
|
||||
) {
|
||||
let handle = match texture {
|
||||
ImageOrPath::Image { label, image } => load_context.add_labeled_asset(label, image),
|
||||
ImageOrPath::Path { path, is_srgb } => {
|
||||
load_context.load_with_settings(path, move |settings: &mut ImageLoaderSettings| {
|
||||
settings.is_srgb = is_srgb;
|
||||
})
|
||||
}
|
||||
};
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// We collect handles to ensure loaded images from paths are not unloaded before they are used elsewhere
|
||||
// in the loader. This prevents "reloads", but it also prevents dropping the is_srgb context on reload.
|
||||
//
|
||||
// In theory we could store a mapping between texture.index() and handle to use
|
||||
// later in the loader when looking up handles for materials. However this would mean
|
||||
// that the material's load context would no longer track those images as dependencies.
|
||||
let mut _texture_handles = Vec::new();
|
||||
if gltf.textures().len() == 1 || cfg!(target_arch = "wasm32") {
|
||||
for texture in gltf.textures() {
|
||||
let parent_path = load_context.path().parent().unwrap();
|
||||
let image = load_image(
|
||||
texture,
|
||||
&buffer_data,
|
||||
&linear_textures,
|
||||
parent_path,
|
||||
loader.supported_compressed_formats,
|
||||
)
|
||||
.await?;
|
||||
process_loaded_texture(load_context, &mut _texture_handles, image);
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
IoTaskPool::get()
|
||||
.scope(|scope| {
|
||||
gltf.textures().for_each(|gltf_texture| {
|
||||
let parent_path = load_context.path().parent().unwrap();
|
||||
let linear_textures = &linear_textures;
|
||||
let buffer_data = &buffer_data;
|
||||
scope.spawn(async move {
|
||||
load_image(
|
||||
gltf_texture,
|
||||
buffer_data,
|
||||
linear_textures,
|
||||
parent_path,
|
||||
loader.supported_compressed_formats,
|
||||
)
|
||||
.await
|
||||
});
|
||||
});
|
||||
})
|
||||
.into_iter()
|
||||
.for_each(|result| match result {
|
||||
Ok(image) => {
|
||||
process_loaded_texture(load_context, &mut _texture_handles, image);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Error loading glTF texture: {}", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut materials = vec![];
|
||||
let mut named_materials = HashMap::default();
|
||||
// NOTE: materials must be loaded after textures because image load() calls will happen before load_with_settings, preventing is_srgb from being set properly
|
||||
for material in gltf.materials() {
|
||||
let handle = load_material(&material, load_context);
|
||||
if let Some(name) = material.name() {
|
||||
named_materials.insert(name.to_string(), handle.clone());
|
||||
}
|
||||
materials.push(handle);
|
||||
}
|
||||
|
||||
let mut meshes = vec![];
|
||||
let mut named_meshes = HashMap::default();
|
||||
for gltf_mesh in gltf.meshes() {
|
||||
|
@ -255,10 +344,8 @@ async fn load_gltf<'a, 'b>(
|
|||
morph_target_reader.map(PrimitiveMorphAttributesIter),
|
||||
mesh.count_vertices(),
|
||||
)?;
|
||||
let handle = load_context.set_labeled_asset(
|
||||
&morph_targets_label,
|
||||
LoadedAsset::new(morph_target_image.0),
|
||||
);
|
||||
let handle =
|
||||
load_context.add_labeled_asset(morph_targets_label, morph_target_image.0);
|
||||
|
||||
mesh.set_morph_targets(handle);
|
||||
let extras = gltf_mesh.extras().as_ref();
|
||||
|
@ -306,7 +393,7 @@ async fn load_gltf<'a, 'b>(
|
|||
}
|
||||
}
|
||||
|
||||
let mesh = load_context.set_labeled_asset(&primitive_label, LoadedAsset::new(mesh));
|
||||
let mesh = load_context.add_labeled_asset(primitive_label, mesh);
|
||||
primitives.push(super::GltfPrimitive {
|
||||
mesh,
|
||||
material: primitive
|
||||
|
@ -318,12 +405,12 @@ async fn load_gltf<'a, 'b>(
|
|||
});
|
||||
}
|
||||
|
||||
let handle = load_context.set_labeled_asset(
|
||||
&mesh_label(&gltf_mesh),
|
||||
LoadedAsset::new(super::GltfMesh {
|
||||
let handle = load_context.add_labeled_asset(
|
||||
mesh_label(&gltf_mesh),
|
||||
super::GltfMesh {
|
||||
primitives,
|
||||
extras: get_gltf_extras(gltf_mesh.extras()),
|
||||
}),
|
||||
},
|
||||
);
|
||||
if let Some(name) = gltf_mesh.name() {
|
||||
named_meshes.insert(name.to_string(), handle.clone());
|
||||
|
@ -369,7 +456,7 @@ async fn load_gltf<'a, 'b>(
|
|||
}
|
||||
let nodes = resolve_node_hierarchy(nodes_intermediate, load_context.path())
|
||||
.into_iter()
|
||||
.map(|(label, node)| load_context.set_labeled_asset(&label, LoadedAsset::new(node)))
|
||||
.map(|(label, node)| load_context.add_labeled_asset(label, node))
|
||||
.collect::<Vec<bevy_asset::Handle<GltfNode>>>();
|
||||
let named_nodes = named_nodes_intermediate
|
||||
.into_iter()
|
||||
|
@ -380,54 +467,6 @@ async fn load_gltf<'a, 'b>(
|
|||
})
|
||||
.collect();
|
||||
|
||||
// TODO: use the threaded impl on wasm once wasm thread pool doesn't deadlock on it
|
||||
// See https://github.com/bevyengine/bevy/issues/1924 for more details
|
||||
// The taskpool use is also avoided when there is only one texture for performance reasons and
|
||||
// to avoid https://github.com/bevyengine/bevy/pull/2725
|
||||
if gltf.textures().len() == 1 || cfg!(target_arch = "wasm32") {
|
||||
for gltf_texture in gltf.textures() {
|
||||
let (texture, label) = load_texture(
|
||||
gltf_texture,
|
||||
&buffer_data,
|
||||
&linear_textures,
|
||||
load_context,
|
||||
loader.supported_compressed_formats,
|
||||
)
|
||||
.await?;
|
||||
load_context.set_labeled_asset(&label, LoadedAsset::new(texture));
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
IoTaskPool::get()
|
||||
.scope(|scope| {
|
||||
gltf.textures().for_each(|gltf_texture| {
|
||||
let linear_textures = &linear_textures;
|
||||
let load_context: &LoadContext = load_context;
|
||||
let buffer_data = &buffer_data;
|
||||
scope.spawn(async move {
|
||||
load_texture(
|
||||
gltf_texture,
|
||||
buffer_data,
|
||||
linear_textures,
|
||||
load_context,
|
||||
loader.supported_compressed_formats,
|
||||
)
|
||||
.await
|
||||
});
|
||||
});
|
||||
})
|
||||
.into_iter()
|
||||
.filter_map(|res| {
|
||||
if let Err(err) = res.as_ref() {
|
||||
warn!("Error loading glTF texture: {}", err);
|
||||
}
|
||||
res.ok()
|
||||
})
|
||||
.for_each(|(texture, label)| {
|
||||
load_context.set_labeled_asset(&label, LoadedAsset::new(texture));
|
||||
});
|
||||
}
|
||||
|
||||
let skinned_mesh_inverse_bindposes: Vec<_> = gltf
|
||||
.skins()
|
||||
.map(|gltf_skin| {
|
||||
|
@ -438,9 +477,9 @@ async fn load_gltf<'a, 'b>(
|
|||
.map(|mat| Mat4::from_cols_array_2d(&mat))
|
||||
.collect();
|
||||
|
||||
load_context.set_labeled_asset(
|
||||
&skin_label(&gltf_skin),
|
||||
LoadedAsset::new(SkinnedMeshInverseBindposes::from(inverse_bindposes)),
|
||||
load_context.add_labeled_asset(
|
||||
skin_label(&gltf_skin),
|
||||
SkinnedMeshInverseBindposes::from(inverse_bindposes),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
@ -514,8 +553,7 @@ async fn load_gltf<'a, 'b>(
|
|||
});
|
||||
}
|
||||
|
||||
let scene_handle = load_context
|
||||
.set_labeled_asset(&scene_label(&scene), LoadedAsset::new(Scene::new(world)));
|
||||
let scene_handle = load_context.add_labeled_asset(scene_label(&scene), Scene::new(world));
|
||||
|
||||
if let Some(name) = scene.name() {
|
||||
named_scenes.insert(name.to_string(), scene_handle.clone());
|
||||
|
@ -523,7 +561,7 @@ async fn load_gltf<'a, 'b>(
|
|||
scenes.push(scene_handle);
|
||||
}
|
||||
|
||||
load_context.set_default_asset(LoadedAsset::new(Gltf {
|
||||
Ok(Gltf {
|
||||
default_scene: gltf
|
||||
.default_scene()
|
||||
.and_then(|scene| scenes.get(scene.index()))
|
||||
|
@ -540,9 +578,7 @@ async fn load_gltf<'a, 'b>(
|
|||
animations,
|
||||
#[cfg(feature = "bevy_animation")]
|
||||
named_animations,
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn get_gltf_extras(extras: &gltf::json::Extras) -> Option<GltfExtras> {
|
||||
|
@ -575,107 +611,98 @@ fn paths_recur(
|
|||
}
|
||||
|
||||
/// Loads a glTF texture as a bevy [`Image`] and returns it together with its label.
|
||||
async fn load_texture<'a>(
|
||||
async fn load_image<'a, 'b>(
|
||||
gltf_texture: gltf::Texture<'a>,
|
||||
buffer_data: &[Vec<u8>],
|
||||
linear_textures: &HashSet<usize>,
|
||||
load_context: &LoadContext<'a>,
|
||||
parent_path: &'b Path,
|
||||
supported_compressed_formats: CompressedImageFormats,
|
||||
) -> Result<(Image, String), GltfError> {
|
||||
) -> Result<ImageOrPath, GltfError> {
|
||||
let is_srgb = !linear_textures.contains(&gltf_texture.index());
|
||||
let mut texture = match gltf_texture.source().source() {
|
||||
match gltf_texture.source().source() {
|
||||
gltf::image::Source::View { view, mime_type } => {
|
||||
let start = view.offset();
|
||||
let end = view.offset() + view.length();
|
||||
let buffer = &buffer_data[view.buffer().index()][start..end];
|
||||
Image::from_buffer(
|
||||
let mut image = Image::from_buffer(
|
||||
buffer,
|
||||
ImageType::MimeType(mime_type),
|
||||
supported_compressed_formats,
|
||||
is_srgb,
|
||||
)?
|
||||
)?;
|
||||
image.sampler_descriptor = ImageSampler::Descriptor(texture_sampler(&gltf_texture));
|
||||
Ok(ImageOrPath::Image {
|
||||
image,
|
||||
label: texture_label(&gltf_texture),
|
||||
})
|
||||
}
|
||||
gltf::image::Source::Uri { uri, mime_type } => {
|
||||
let uri = percent_encoding::percent_decode_str(uri)
|
||||
.decode_utf8()
|
||||
.unwrap();
|
||||
let uri = uri.as_ref();
|
||||
let (bytes, image_type) = if let Ok(data_uri) = DataUri::parse(uri) {
|
||||
(data_uri.decode()?, ImageType::MimeType(data_uri.mime_type))
|
||||
if let Ok(data_uri) = DataUri::parse(uri) {
|
||||
let bytes = data_uri.decode()?;
|
||||
let image_type = ImageType::MimeType(data_uri.mime_type);
|
||||
Ok(ImageOrPath::Image {
|
||||
image: Image::from_buffer(
|
||||
&bytes,
|
||||
mime_type.map(ImageType::MimeType).unwrap_or(image_type),
|
||||
supported_compressed_formats,
|
||||
is_srgb,
|
||||
)?,
|
||||
label: texture_label(&gltf_texture),
|
||||
})
|
||||
} else {
|
||||
let parent = load_context.path().parent().unwrap();
|
||||
let image_path = parent.join(uri);
|
||||
let bytes = load_context.read_asset_bytes(image_path.clone()).await?;
|
||||
|
||||
let extension = Path::new(uri).extension().unwrap().to_str().unwrap();
|
||||
let image_type = ImageType::Extension(extension);
|
||||
|
||||
(bytes, image_type)
|
||||
};
|
||||
|
||||
Image::from_buffer(
|
||||
&bytes,
|
||||
mime_type.map(ImageType::MimeType).unwrap_or(image_type),
|
||||
supported_compressed_formats,
|
||||
is_srgb,
|
||||
)?
|
||||
let image_path = parent_path.join(uri);
|
||||
Ok(ImageOrPath::Path {
|
||||
path: image_path,
|
||||
is_srgb,
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
texture.sampler_descriptor = ImageSampler::Descriptor(texture_sampler(&gltf_texture));
|
||||
|
||||
Ok((texture, texture_label(&gltf_texture)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a glTF material as a bevy [`StandardMaterial`] and returns it.
|
||||
fn load_material(material: &Material, load_context: &mut LoadContext) -> Handle<StandardMaterial> {
|
||||
let material_label = material_label(material);
|
||||
load_context.labeled_asset_scope(material_label, |load_context| {
|
||||
let pbr = material.pbr_metallic_roughness();
|
||||
|
||||
let pbr = material.pbr_metallic_roughness();
|
||||
|
||||
let color = pbr.base_color_factor();
|
||||
let base_color_texture = pbr.base_color_texture().map(|info| {
|
||||
// TODO: handle info.tex_coord() (the *set* index for the right texcoords)
|
||||
let label = texture_label(&info.texture());
|
||||
let path = AssetPath::new_ref(load_context.path(), Some(&label));
|
||||
load_context.get_handle(path)
|
||||
});
|
||||
|
||||
let normal_map_texture: Option<Handle<Image>> =
|
||||
material.normal_texture().map(|normal_texture| {
|
||||
// TODO: handle normal_texture.scale
|
||||
// TODO: handle normal_texture.tex_coord() (the *set* index for the right texcoords)
|
||||
let label = texture_label(&normal_texture.texture());
|
||||
let path = AssetPath::new_ref(load_context.path(), Some(&label));
|
||||
load_context.get_handle(path)
|
||||
// TODO: handle missing label handle errors here?
|
||||
let color = pbr.base_color_factor();
|
||||
let base_color_texture = pbr.base_color_texture().map(|info| {
|
||||
// TODO: handle info.tex_coord() (the *set* index for the right texcoords)
|
||||
texture_handle(load_context, &info.texture())
|
||||
});
|
||||
|
||||
let metallic_roughness_texture = pbr.metallic_roughness_texture().map(|info| {
|
||||
// TODO: handle info.tex_coord() (the *set* index for the right texcoords)
|
||||
let label = texture_label(&info.texture());
|
||||
let path = AssetPath::new_ref(load_context.path(), Some(&label));
|
||||
load_context.get_handle(path)
|
||||
});
|
||||
let normal_map_texture: Option<Handle<Image>> =
|
||||
material.normal_texture().map(|normal_texture| {
|
||||
// TODO: handle normal_texture.scale
|
||||
// TODO: handle normal_texture.tex_coord() (the *set* index for the right texcoords)
|
||||
texture_handle(load_context, &normal_texture.texture())
|
||||
});
|
||||
|
||||
let occlusion_texture = material.occlusion_texture().map(|occlusion_texture| {
|
||||
// TODO: handle occlusion_texture.tex_coord() (the *set* index for the right texcoords)
|
||||
// TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength)
|
||||
let label = texture_label(&occlusion_texture.texture());
|
||||
let path = AssetPath::new_ref(load_context.path(), Some(&label));
|
||||
load_context.get_handle(path)
|
||||
});
|
||||
let metallic_roughness_texture = pbr.metallic_roughness_texture().map(|info| {
|
||||
// TODO: handle info.tex_coord() (the *set* index for the right texcoords)
|
||||
texture_handle(load_context, &info.texture())
|
||||
});
|
||||
|
||||
let emissive = material.emissive_factor();
|
||||
let emissive_texture = material.emissive_texture().map(|info| {
|
||||
// TODO: handle occlusion_texture.tex_coord() (the *set* index for the right texcoords)
|
||||
// TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength)
|
||||
let label = texture_label(&info.texture());
|
||||
let path = AssetPath::new_ref(load_context.path(), Some(&label));
|
||||
load_context.get_handle(path)
|
||||
});
|
||||
let occlusion_texture = material.occlusion_texture().map(|occlusion_texture| {
|
||||
// TODO: handle occlusion_texture.tex_coord() (the *set* index for the right texcoords)
|
||||
// TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength)
|
||||
texture_handle(load_context, &occlusion_texture.texture())
|
||||
});
|
||||
|
||||
load_context.set_labeled_asset(
|
||||
&material_label,
|
||||
LoadedAsset::new(StandardMaterial {
|
||||
let emissive = material.emissive_factor();
|
||||
let emissive_texture = material.emissive_texture().map(|info| {
|
||||
// TODO: handle occlusion_texture.tex_coord() (the *set* index for the right texcoords)
|
||||
// TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength)
|
||||
texture_handle(load_context, &info.texture())
|
||||
});
|
||||
|
||||
StandardMaterial {
|
||||
base_color: Color::rgba_linear(color[0], color[1], color[2], color[3]),
|
||||
base_color_texture,
|
||||
perceptual_roughness: pbr.roughness_factor(),
|
||||
|
@ -695,8 +722,8 @@ fn load_material(material: &Material, load_context: &mut LoadContext) -> Handle<
|
|||
unlit: material.unlit(),
|
||||
alpha_mode: alpha_mode(material),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Loads a glTF node.
|
||||
|
@ -771,8 +798,8 @@ fn load_node(
|
|||
if let Some(weights) = mesh.weights() {
|
||||
let first_mesh = if let Some(primitive) = mesh.primitives().next() {
|
||||
let primitive_label = primitive_label(&mesh, &primitive);
|
||||
let path = AssetPath::new_ref(load_context.path(), Some(&primitive_label));
|
||||
Some(Handle::weak(HandleId::from(path)))
|
||||
let handle: Handle<Mesh> = load_context.get_label_handle(&primitive_label);
|
||||
Some(handle)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -796,14 +823,11 @@ fn load_node(
|
|||
|
||||
let primitive_label = primitive_label(&mesh, &primitive);
|
||||
let bounds = primitive.bounding_box();
|
||||
let mesh_asset_path =
|
||||
AssetPath::new_ref(load_context.path(), Some(&primitive_label));
|
||||
let material_asset_path =
|
||||
AssetPath::new_ref(load_context.path(), Some(&material_label));
|
||||
|
||||
let mut primitive_entity = parent.spawn(PbrBundle {
|
||||
mesh: load_context.get_handle(mesh_asset_path),
|
||||
material: load_context.get_handle(material_asset_path),
|
||||
let mut mesh_entity = parent.spawn(PbrBundle {
|
||||
// TODO: handle missing label handle errors here?
|
||||
mesh: load_context.get_label_handle(&primitive_label),
|
||||
material: load_context.get_label_handle(&material_label),
|
||||
..Default::default()
|
||||
});
|
||||
let target_count = primitive.morph_targets().len();
|
||||
|
@ -818,23 +842,23 @@ fn load_node(
|
|||
// they should all have the same length.
|
||||
// > All morph target accessors MUST have the same count as
|
||||
// > the accessors of the original primitive.
|
||||
primitive_entity.insert(MeshMorphWeights::new(weights).unwrap());
|
||||
mesh_entity.insert(MeshMorphWeights::new(weights).unwrap());
|
||||
}
|
||||
primitive_entity.insert(Aabb::from_min_max(
|
||||
mesh_entity.insert(Aabb::from_min_max(
|
||||
Vec3::from_slice(&bounds.min),
|
||||
Vec3::from_slice(&bounds.max),
|
||||
));
|
||||
|
||||
if let Some(extras) = primitive.extras() {
|
||||
primitive_entity.insert(super::GltfExtras {
|
||||
mesh_entity.insert(super::GltfExtras {
|
||||
value: extras.get().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
primitive_entity.insert(Name::new(primitive_name(&mesh, &primitive)));
|
||||
mesh_entity.insert(Name::new(primitive_name(&mesh, &primitive)));
|
||||
// Mark for adding skinned mesh
|
||||
if let Some(skin) = gltf_node.skin() {
|
||||
entity_to_skin_index_map.insert(primitive_entity.id(), skin.index());
|
||||
entity_to_skin_index_map.insert(mesh_entity.id(), skin.index());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -979,6 +1003,29 @@ fn texture_label(texture: &gltf::Texture) -> String {
|
|||
format!("Texture{}", texture.index())
|
||||
}
|
||||
|
||||
fn texture_handle(load_context: &mut LoadContext, texture: &gltf::Texture) -> Handle<Image> {
|
||||
match texture.source().source() {
|
||||
gltf::image::Source::View { .. } => {
|
||||
let label = texture_label(texture);
|
||||
load_context.get_label_handle(&label)
|
||||
}
|
||||
gltf::image::Source::Uri { uri, .. } => {
|
||||
let uri = percent_encoding::percent_decode_str(uri)
|
||||
.decode_utf8()
|
||||
.unwrap();
|
||||
let uri = uri.as_ref();
|
||||
if let Ok(_data_uri) = DataUri::parse(uri) {
|
||||
let label = texture_label(texture);
|
||||
load_context.get_label_handle(&label)
|
||||
} else {
|
||||
let parent = load_context.path().parent().unwrap();
|
||||
let image_path = parent.join(uri);
|
||||
load_context.load(image_path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the label for the `node`.
|
||||
fn node_label(node: &gltf::Node) -> String {
|
||||
format!("Node{}", node.index())
|
||||
|
@ -1070,8 +1117,7 @@ fn alpha_mode(material: &Material) -> AlphaMode {
|
|||
/// Loads the raw glTF buffer data for a specific glTF file.
|
||||
async fn load_buffers(
|
||||
gltf: &gltf::Gltf,
|
||||
load_context: &LoadContext<'_>,
|
||||
asset_path: &Path,
|
||||
load_context: &mut LoadContext<'_>,
|
||||
) -> Result<Vec<Vec<u8>>, GltfError> {
|
||||
const VALID_MIME_TYPES: &[&str] = &["application/octet-stream", "application/gltf-buffer"];
|
||||
|
||||
|
@ -1090,8 +1136,8 @@ async fn load_buffers(
|
|||
Ok(_) => return Err(GltfError::BufferFormatUnsupported),
|
||||
Err(()) => {
|
||||
// TODO: Remove this and add dep
|
||||
let buffer_path = asset_path.parent().unwrap().join(uri);
|
||||
load_context.read_asset_bytes(buffer_path).await?
|
||||
let buffer_path = load_context.path().parent().unwrap().join(uri);
|
||||
load_context.read_asset_bytes(&buffer_path).await?
|
||||
}
|
||||
};
|
||||
buffer_data.push(buffer_bytes);
|
||||
|
@ -1164,6 +1210,11 @@ fn resolve_node_hierarchy(
|
|||
.collect()
|
||||
}
|
||||
|
||||
enum ImageOrPath {
|
||||
Image { image: Image, label: String },
|
||||
Path { path: PathBuf, is_srgb: bool },
|
||||
}
|
||||
|
||||
struct DataUri<'a> {
|
||||
mime_type: &'a str,
|
||||
base64: bool,
|
||||
|
|
|
@ -23,7 +23,6 @@ trace_chrome = [ "bevy_log/tracing-chrome" ]
|
|||
trace_tracy = ["bevy_render?/tracing-tracy", "bevy_log/tracing-tracy" ]
|
||||
trace_tracy_memory = ["bevy_log/trace_tracy_memory"]
|
||||
wgpu_trace = ["bevy_render/wgpu_trace"]
|
||||
debug_asset_server = ["bevy_asset/debug_asset_server"]
|
||||
detailed_trace = ["bevy_utils/detailed_trace"]
|
||||
|
||||
# Image format support for texture loading (PNG and HDR are enabled by default)
|
||||
|
@ -62,11 +61,8 @@ symphonia-wav = ["bevy_audio/symphonia-wav"]
|
|||
shader_format_glsl = ["bevy_render/shader_format_glsl"]
|
||||
shader_format_spirv = ["bevy_render/shader_format_spirv"]
|
||||
|
||||
# Enable watching file system for asset hot reload
|
||||
filesystem_watcher = ["bevy_asset/filesystem_watcher"]
|
||||
|
||||
serialize = ["bevy_core/serialize", "bevy_input/serialize", "bevy_time/serialize", "bevy_window/serialize", "bevy_transform/serialize", "bevy_math/serialize", "bevy_scene/serialize"]
|
||||
multi-threaded = ["bevy_ecs/multi-threaded", "bevy_tasks/multi-threaded"]
|
||||
multi-threaded = ["bevy_asset/multi-threaded", "bevy_ecs/multi-threaded", "bevy_tasks/multi-threaded"]
|
||||
|
||||
# Display server protocol support (X11 is enabled by default)
|
||||
wayland = ["bevy_winit/wayland"]
|
||||
|
@ -105,6 +101,9 @@ glam_assert = ["bevy_math/glam_assert"]
|
|||
|
||||
default_font = ["bevy_text?/default_font"]
|
||||
|
||||
# Enables watching the filesystem for Bevy Asset hot-reloading
|
||||
filesystem_watcher = ["bevy_asset?/filesystem_watcher"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_a11y = { path = "../bevy_a11y", version = "0.12.0-dev" }
|
||||
|
|
|
@ -12,7 +12,6 @@ use bevy_app::{PluginGroup, PluginGroupBuilder};
|
|||
/// * [`InputPlugin`](crate::input::InputPlugin)
|
||||
/// * [`WindowPlugin`](crate::window::WindowPlugin)
|
||||
/// * [`AssetPlugin`](crate::asset::AssetPlugin) - with feature `bevy_asset`
|
||||
/// * [`DebugAssetPlugin`](crate::asset::debug_asset_server::DebugAssetServerPlugin) - with feature `debug_asset_server`
|
||||
/// * [`ScenePlugin`](crate::scene::ScenePlugin) - with feature `bevy_scene`
|
||||
/// * [`WinitPlugin`](crate::winit::WinitPlugin) - with feature `bevy_winit`
|
||||
/// * [`RenderPlugin`](crate::render::RenderPlugin) - with feature `bevy_render`
|
||||
|
@ -58,11 +57,6 @@ impl PluginGroup for DefaultPlugins {
|
|||
group = group.add(bevy_asset::AssetPlugin::default());
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug_asset_server")]
|
||||
{
|
||||
group = group.add(bevy_asset::debug_asset_server::DebugAssetServerPlugin);
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_scene")]
|
||||
{
|
||||
group = group.add(bevy_scene::ScenePlugin);
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{load_internal_asset, Handle, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_core_pipeline::prelude::Camera3d;
|
||||
use bevy_ecs::{prelude::Component, query::With};
|
||||
use bevy_reflect::{Reflect, TypeUuid};
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_render::{
|
||||
extract_component::{ExtractComponent, ExtractComponentPlugin},
|
||||
render_asset::RenderAssets,
|
||||
|
@ -13,8 +13,8 @@ use bevy_render::{
|
|||
texture::{FallbackImageCubemap, Image},
|
||||
};
|
||||
|
||||
pub const ENVIRONMENT_MAP_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 154476556247605696);
|
||||
pub const ENVIRONMENT_MAP_SHADER_HANDLE: Handle<Shader> =
|
||||
Handle::weak_from_u128(154476556247605696);
|
||||
|
||||
pub struct EnvironmentMapPlugin;
|
||||
|
||||
|
|
|
@ -52,9 +52,8 @@ pub mod draw_3d_graph {
|
|||
}
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{load_internal_asset, AddAsset, Assets, Handle, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_render::{
|
||||
camera::CameraUpdateSystem, extract_resource::ExtractResourcePlugin, prelude::Color,
|
||||
render_asset::prepare_assets, render_graph::RenderGraph, render_phase::sort_phase_system,
|
||||
|
@ -64,28 +63,18 @@ use bevy_render::{
|
|||
use bevy_transform::TransformSystem;
|
||||
use environment_map::EnvironmentMapPlugin;
|
||||
|
||||
pub const PBR_TYPES_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 1708015359337029744);
|
||||
pub const PBR_BINDINGS_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 5635987986427308186);
|
||||
pub const UTILS_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 1900548483293416725);
|
||||
pub const CLUSTERED_FORWARD_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 166852093121196815);
|
||||
pub const PBR_LIGHTING_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 14170772752254856967);
|
||||
pub const SHADOWS_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 11350275143789590502);
|
||||
pub const PBR_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 4805239651767701046);
|
||||
pub const PBR_PREPASS_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 9407115064344201137);
|
||||
pub const PBR_FUNCTIONS_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 16550102964439850292);
|
||||
pub const PBR_AMBIENT_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2441520459096337034);
|
||||
pub const PARALLAX_MAPPING_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 17035894873630133905);
|
||||
pub const PBR_TYPES_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(1708015359337029744);
|
||||
pub const PBR_BINDINGS_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(5635987986427308186);
|
||||
pub const UTILS_HANDLE: Handle<Shader> = Handle::weak_from_u128(1900548483293416725);
|
||||
pub const CLUSTERED_FORWARD_HANDLE: Handle<Shader> = Handle::weak_from_u128(166852093121196815);
|
||||
pub const PBR_LIGHTING_HANDLE: Handle<Shader> = Handle::weak_from_u128(14170772752254856967);
|
||||
pub const SHADOWS_HANDLE: Handle<Shader> = Handle::weak_from_u128(11350275143789590502);
|
||||
pub const PBR_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(4805239651767701046);
|
||||
pub const PBR_PREPASS_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(9407115064344201137);
|
||||
pub const PBR_FUNCTIONS_HANDLE: Handle<Shader> = Handle::weak_from_u128(16550102964439850292);
|
||||
pub const PBR_AMBIENT_HANDLE: Handle<Shader> = Handle::weak_from_u128(2441520459096337034);
|
||||
pub const PARALLAX_MAPPING_SHADER_HANDLE: Handle<Shader> =
|
||||
Handle::weak_from_u128(17035894873630133905);
|
||||
|
||||
/// Sets up the entire PBR infrastructure of bevy.
|
||||
pub struct PbrPlugin {
|
||||
|
@ -247,16 +236,14 @@ impl Plugin for PbrPlugin {
|
|||
),
|
||||
);
|
||||
|
||||
app.world
|
||||
.resource_mut::<Assets<StandardMaterial>>()
|
||||
.set_untracked(
|
||||
Handle::<StandardMaterial>::default(),
|
||||
StandardMaterial {
|
||||
base_color: Color::rgb(1.0, 0.0, 0.5),
|
||||
unlit: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
app.world.resource_mut::<Assets<StandardMaterial>>().insert(
|
||||
Handle::<StandardMaterial>::default(),
|
||||
StandardMaterial {
|
||||
base_color: Color::rgb(1.0, 0.0, 0.5),
|
||||
unlit: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let render_app = match app.get_sub_app_mut(RenderApp) {
|
||||
Ok(render_app) => render_app,
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
SetMeshBindGroup, SetMeshViewBindGroup, Shadow,
|
||||
};
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle};
|
||||
use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, AssetServer, Assets, Handle};
|
||||
use bevy_core_pipeline::{
|
||||
core_3d::{AlphaMask3d, Opaque3d, Transparent3d},
|
||||
experimental::taa::TemporalAntiAliasSettings,
|
||||
|
@ -19,7 +19,6 @@ use bevy_ecs::{
|
|||
SystemParamItem,
|
||||
},
|
||||
};
|
||||
use bevy_reflect::{TypePath, TypeUuid};
|
||||
use bevy_render::{
|
||||
extract_component::ExtractComponentPlugin,
|
||||
mesh::{Mesh, MeshVertexBufferLayout},
|
||||
|
@ -50,8 +49,6 @@ use std::marker::PhantomData;
|
|||
/// Materials must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders.
|
||||
/// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details.
|
||||
///
|
||||
/// Materials must also implement [`TypeUuid`] so they can be treated as an [`Asset`](bevy_asset::Asset).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Here is a simple Material implementation. The [`AsBindGroup`] derive has many features. To see what else is available,
|
||||
|
@ -61,10 +58,9 @@ use std::marker::PhantomData;
|
|||
/// # use bevy_ecs::prelude::*;
|
||||
/// # use bevy_reflect::{TypeUuid, TypePath};
|
||||
/// # use bevy_render::{render_resource::{AsBindGroup, ShaderRef}, texture::Image, color::Color};
|
||||
/// # use bevy_asset::{Handle, AssetServer, Assets};
|
||||
/// # use bevy_asset::{Handle, AssetServer, Assets, Asset};
|
||||
///
|
||||
/// #[derive(AsBindGroup, TypeUuid, TypePath, Debug, Clone)]
|
||||
/// #[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"]
|
||||
/// #[derive(AsBindGroup, Debug, Clone, Asset, TypePath)]
|
||||
/// pub struct CustomMaterial {
|
||||
/// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to
|
||||
/// // its shader-compatible equivalent. Most core math types already implement `ShaderType`.
|
||||
|
@ -106,7 +102,7 @@ use std::marker::PhantomData;
|
|||
/// @group(1) @binding(2)
|
||||
/// var color_sampler: sampler;
|
||||
/// ```
|
||||
pub trait Material: AsBindGroup + Send + Sync + Clone + TypeUuid + TypePath + Sized {
|
||||
pub trait Material: Asset + AsBindGroup + Clone + Sized {
|
||||
/// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the default mesh vertex shader
|
||||
/// will be used.
|
||||
fn vertex_shader() -> ShaderRef {
|
||||
|
@ -187,7 +183,7 @@ where
|
|||
M::Data: PartialEq + Eq + Hash + Clone,
|
||||
{
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_asset::<M>()
|
||||
app.init_asset::<M>()
|
||||
.add_plugins(ExtractComponentPlugin::<Handle<M>>::extract_visible());
|
||||
|
||||
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
|
@ -366,7 +362,7 @@ impl<P: PhaseItem, M: Material, const I: usize> RenderCommand<P> for SetMaterial
|
|||
materials: SystemParamItem<'w, '_, Self::Param>,
|
||||
pass: &mut TrackedRenderPass<'w>,
|
||||
) -> RenderCommandResult {
|
||||
let material = materials.into_inner().get(material_handle).unwrap();
|
||||
let material = materials.into_inner().get(&material_handle.id()).unwrap();
|
||||
pass.set_bind_group(I, &material.bind_group, &[]);
|
||||
RenderCommandResult::Success
|
||||
}
|
||||
|
@ -472,7 +468,7 @@ pub fn queue_material_meshes<M: Material>(
|
|||
{
|
||||
if let (Some(mesh), Some(material)) = (
|
||||
render_meshes.get(mesh_handle),
|
||||
render_materials.get(material_handle),
|
||||
render_materials.get(&material_handle.id()),
|
||||
) {
|
||||
let mut mesh_key =
|
||||
MeshPipelineKey::from_primitive_topology(mesh.primitive_topology)
|
||||
|
@ -576,8 +572,8 @@ pub struct PreparedMaterial<T: Material> {
|
|||
|
||||
#[derive(Resource)]
|
||||
pub struct ExtractedMaterials<M: Material> {
|
||||
extracted: Vec<(Handle<M>, M)>,
|
||||
removed: Vec<Handle<M>>,
|
||||
extracted: Vec<(AssetId<M>, M)>,
|
||||
removed: Vec<AssetId<M>>,
|
||||
}
|
||||
|
||||
impl<M: Material> Default for ExtractedMaterials<M> {
|
||||
|
@ -591,7 +587,7 @@ impl<M: Material> Default for ExtractedMaterials<M> {
|
|||
|
||||
/// Stores all prepared representations of [`Material`] assets for as long as they exist.
|
||||
#[derive(Resource, Deref, DerefMut)]
|
||||
pub struct RenderMaterials<T: Material>(pub HashMap<Handle<T>, PreparedMaterial<T>>);
|
||||
pub struct RenderMaterials<T: Material>(pub HashMap<AssetId<T>, PreparedMaterial<T>>);
|
||||
|
||||
impl<T: Material> Default for RenderMaterials<T> {
|
||||
fn default() -> Self {
|
||||
|
@ -610,20 +606,23 @@ pub fn extract_materials<M: Material>(
|
|||
let mut removed = Vec::new();
|
||||
for event in events.read() {
|
||||
match event {
|
||||
AssetEvent::Created { handle } | AssetEvent::Modified { handle } => {
|
||||
changed_assets.insert(handle.clone_weak());
|
||||
AssetEvent::Added { id } | AssetEvent::Modified { id } => {
|
||||
changed_assets.insert(*id);
|
||||
}
|
||||
AssetEvent::Removed { handle } => {
|
||||
changed_assets.remove(handle);
|
||||
removed.push(handle.clone_weak());
|
||||
AssetEvent::Removed { id } => {
|
||||
changed_assets.remove(id);
|
||||
removed.push(*id);
|
||||
}
|
||||
AssetEvent::LoadedWithDependencies { .. } => {
|
||||
// TODO: handle this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut extracted_assets = Vec::new();
|
||||
for handle in changed_assets.drain() {
|
||||
if let Some(asset) = assets.get(&handle) {
|
||||
extracted_assets.push((handle, asset.clone()));
|
||||
for id in changed_assets.drain() {
|
||||
if let Some(asset) = assets.get(id) {
|
||||
extracted_assets.push((id, asset.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -635,7 +634,7 @@ pub fn extract_materials<M: Material>(
|
|||
|
||||
/// All [`Material`] values of a given type that should be prepared next frame.
|
||||
pub struct PrepareNextFrameMaterials<M: Material> {
|
||||
assets: Vec<(Handle<M>, M)>,
|
||||
assets: Vec<(AssetId<M>, M)>,
|
||||
}
|
||||
|
||||
impl<M: Material> Default for PrepareNextFrameMaterials<M> {
|
||||
|
@ -658,7 +657,7 @@ pub fn prepare_materials<M: Material>(
|
|||
pipeline: Res<MaterialPipeline<M>>,
|
||||
) {
|
||||
let queued_assets = std::mem::take(&mut prepare_next_frame.assets);
|
||||
for (handle, material) in queued_assets.into_iter() {
|
||||
for (id, material) in queued_assets.into_iter() {
|
||||
match prepare_material(
|
||||
&material,
|
||||
&render_device,
|
||||
|
@ -667,10 +666,10 @@ pub fn prepare_materials<M: Material>(
|
|||
&pipeline,
|
||||
) {
|
||||
Ok(prepared_asset) => {
|
||||
render_materials.insert(handle, prepared_asset);
|
||||
render_materials.insert(id, prepared_asset);
|
||||
}
|
||||
Err(AsBindGroupError::RetryNextUpdate) => {
|
||||
prepare_next_frame.assets.push((handle, material));
|
||||
prepare_next_frame.assets.push((id, material));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -679,7 +678,7 @@ pub fn prepare_materials<M: Material>(
|
|||
render_materials.remove(&removed);
|
||||
}
|
||||
|
||||
for (handle, material) in std::mem::take(&mut extracted_assets.extracted) {
|
||||
for (id, material) in std::mem::take(&mut extracted_assets.extracted) {
|
||||
match prepare_material(
|
||||
&material,
|
||||
&render_device,
|
||||
|
@ -688,10 +687,10 @@ pub fn prepare_materials<M: Material>(
|
|||
&pipeline,
|
||||
) {
|
||||
Ok(prepared_asset) => {
|
||||
render_materials.insert(handle, prepared_asset);
|
||||
render_materials.insert(id, prepared_asset);
|
||||
}
|
||||
Err(AsBindGroupError::RetryNextUpdate) => {
|
||||
prepare_next_frame.assets.push((handle, material));
|
||||
prepare_next_frame.assets.push((id, material));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ use crate::{
|
|||
AlphaMode, Material, MaterialPipeline, MaterialPipelineKey, ParallaxMappingMethod,
|
||||
PBR_PREPASS_SHADER_HANDLE, PBR_SHADER_HANDLE,
|
||||
};
|
||||
use bevy_asset::Handle;
|
||||
use bevy_asset::{Asset, Handle};
|
||||
use bevy_math::Vec4;
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypeUuid};
|
||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
|
||||
use bevy_render::{
|
||||
color::Color, mesh::MeshVertexBufferLayout, render_asset::RenderAssets, render_resource::*,
|
||||
texture::Image,
|
||||
|
@ -15,8 +15,7 @@ use bevy_render::{
|
|||
/// <https://google.github.io/filament/Material%20Properties.pdf>.
|
||||
///
|
||||
/// May be created directly from a [`Color`] or an [`Image`].
|
||||
#[derive(AsBindGroup, Reflect, Debug, Clone, TypeUuid)]
|
||||
#[uuid = "7494888b-c082-457b-aacf-517228cc0c22"]
|
||||
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
|
||||
#[bind_group_data(StandardMaterialKey)]
|
||||
#[uniform(0, StandardMaterialUniform)]
|
||||
#[reflect(Default, Debug)]
|
||||
|
@ -45,6 +44,7 @@ pub struct StandardMaterial {
|
|||
/// [`base_color`]: StandardMaterial::base_color
|
||||
#[texture(1)]
|
||||
#[sampler(2)]
|
||||
#[dependency]
|
||||
pub base_color_texture: Option<Handle<Image>>,
|
||||
|
||||
// Use a color for user friendliness even though we technically don't use the alpha channel
|
||||
|
@ -74,6 +74,7 @@ pub struct StandardMaterial {
|
|||
/// [`emissive`]: StandardMaterial::emissive
|
||||
#[texture(3)]
|
||||
#[sampler(4)]
|
||||
#[dependency]
|
||||
pub emissive_texture: Option<Handle<Image>>,
|
||||
|
||||
/// Linear perceptual roughness, clamped to `[0.089, 1.0]` in the shader.
|
||||
|
@ -122,6 +123,7 @@ pub struct StandardMaterial {
|
|||
/// [`perceptual_roughness`]: StandardMaterial::perceptual_roughness
|
||||
#[texture(5)]
|
||||
#[sampler(6)]
|
||||
#[dependency]
|
||||
pub metallic_roughness_texture: Option<Handle<Image>>,
|
||||
|
||||
/// Specular intensity for non-metals on a linear scale of `[0.0, 1.0]`.
|
||||
|
@ -157,6 +159,7 @@ pub struct StandardMaterial {
|
|||
/// [`Mesh::generate_tangents`]: bevy_render::mesh::Mesh::generate_tangents
|
||||
#[texture(9)]
|
||||
#[sampler(10)]
|
||||
#[dependency]
|
||||
pub normal_map_texture: Option<Handle<Image>>,
|
||||
|
||||
/// Normal map textures authored for DirectX have their y-component flipped. Set this to flip
|
||||
|
@ -175,6 +178,7 @@ pub struct StandardMaterial {
|
|||
/// This is similar to ambient occlusion, but built into the model.
|
||||
#[texture(7)]
|
||||
#[sampler(8)]
|
||||
#[dependency]
|
||||
pub occlusion_texture: Option<Handle<Image>>,
|
||||
|
||||
/// Support two-sided lighting by automatically flipping the normals for "back" faces
|
||||
|
@ -278,6 +282,7 @@ pub struct StandardMaterial {
|
|||
/// [`max_parallax_layer_count`]: StandardMaterial::max_parallax_layer_count
|
||||
#[texture(11)]
|
||||
#[sampler(12)]
|
||||
#[dependency]
|
||||
pub depth_map: Option<Handle<Image>>,
|
||||
|
||||
/// How deep the offset introduced by the depth map should be.
|
||||
|
@ -465,7 +470,8 @@ impl AsBindGroupShaderType<StandardMaterialUniform> for StandardMaterial {
|
|||
}
|
||||
let has_normal_map = self.normal_map_texture.is_some();
|
||||
if has_normal_map {
|
||||
if let Some(texture) = images.get(self.normal_map_texture.as_ref().unwrap()) {
|
||||
let normal_map_id = self.normal_map_texture.as_ref().map(|h| h.id()).unwrap();
|
||||
if let Some(texture) = images.get(normal_map_id) {
|
||||
match texture.texture_format {
|
||||
// All 2-component unorm formats
|
||||
TextureFormat::Rg8Unorm
|
||||
|
@ -561,11 +567,11 @@ impl Material for StandardMaterial {
|
|||
}
|
||||
|
||||
fn prepass_fragment_shader() -> ShaderRef {
|
||||
PBR_PREPASS_SHADER_HANDLE.typed().into()
|
||||
PBR_PREPASS_SHADER_HANDLE.into()
|
||||
}
|
||||
|
||||
fn fragment_shader() -> ShaderRef {
|
||||
PBR_SHADER_HANDLE.typed().into()
|
||||
PBR_SHADER_HANDLE.into()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use bevy_app::{Plugin, PreUpdate};
|
||||
use bevy_asset::{load_internal_asset, AssetServer, Handle, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, AssetServer, Handle};
|
||||
use bevy_core_pipeline::{
|
||||
prelude::Camera3d,
|
||||
prepass::{
|
||||
|
@ -16,7 +16,6 @@ use bevy_ecs::{
|
|||
},
|
||||
};
|
||||
use bevy_math::{Affine3A, Mat4};
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_render::{
|
||||
globals::{GlobalsBuffer, GlobalsUniform},
|
||||
mesh::MeshVertexBufferLayout,
|
||||
|
@ -52,14 +51,12 @@ use crate::{
|
|||
|
||||
use std::{hash::Hash, marker::PhantomData};
|
||||
|
||||
pub const PREPASS_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 921124473254008983);
|
||||
pub const PREPASS_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(921124473254008983);
|
||||
|
||||
pub const PREPASS_BINDINGS_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 5533152893177403494);
|
||||
pub const PREPASS_BINDINGS_SHADER_HANDLE: Handle<Shader> =
|
||||
Handle::weak_from_u128(5533152893177403494);
|
||||
|
||||
pub const PREPASS_UTILS_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 4603948296044544);
|
||||
pub const PREPASS_UTILS_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(4603948296044544);
|
||||
|
||||
/// Sets up everything required to use the prepass pipeline.
|
||||
///
|
||||
|
@ -463,7 +460,7 @@ where
|
|||
// Use the fragment shader from the material
|
||||
let frag_shader_handle = match self.material_fragment_shader.clone() {
|
||||
Some(frag_shader_handle) => frag_shader_handle,
|
||||
_ => PREPASS_SHADER_HANDLE.typed::<Shader>(),
|
||||
_ => PREPASS_SHADER_HANDLE,
|
||||
};
|
||||
|
||||
FragmentState {
|
||||
|
@ -478,7 +475,7 @@ where
|
|||
let vert_shader_handle = if let Some(handle) = &self.material_vertex_shader {
|
||||
handle.clone()
|
||||
} else {
|
||||
PREPASS_SHADER_HANDLE.typed::<Shader>()
|
||||
PREPASS_SHADER_HANDLE
|
||||
};
|
||||
|
||||
let mut push_constant_ranges = Vec::with_capacity(1);
|
||||
|
@ -805,7 +802,7 @@ pub fn queue_prepass_material_meshes<M: Material>(
|
|||
};
|
||||
|
||||
let (Some(material), Some(mesh)) = (
|
||||
render_materials.get(material_handle),
|
||||
render_materials.get(&material_handle.id()),
|
||||
render_meshes.get(mesh_handle),
|
||||
) else {
|
||||
continue;
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{load_internal_asset, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_math::{Vec3, Vec4};
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_render::{
|
||||
extract_component::ExtractComponentPlugin,
|
||||
render_resource::{DynamicUniformBuffer, Shader, ShaderType},
|
||||
|
@ -121,8 +120,7 @@ pub struct ViewFogUniformOffset {
|
|||
}
|
||||
|
||||
/// Handle for the fog WGSL Shader internal asset
|
||||
pub const FOG_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 4913569193382610166);
|
||||
pub const FOG_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(4913569193382610166);
|
||||
|
||||
/// A plugin that consolidates fog extraction, preparation and related resources/assets
|
||||
pub struct FogPlugin;
|
||||
|
|
|
@ -1597,7 +1597,7 @@ pub fn queue_shadows<M: Material>(
|
|||
if let Ok((mesh_handle, material_handle)) = casting_meshes.get(entity) {
|
||||
if let (Some(mesh), Some(material)) = (
|
||||
render_meshes.get(mesh_handle),
|
||||
render_materials.get(material_handle),
|
||||
render_materials.get(&material_handle.id()),
|
||||
) {
|
||||
let mut mesh_key =
|
||||
MeshPipelineKey::from_primitive_topology(mesh.primitive_topology)
|
||||
|
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS,
|
||||
};
|
||||
use bevy_app::Plugin;
|
||||
use bevy_asset::{load_internal_asset, Assets, Handle, HandleId, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, AssetId, Assets, Handle};
|
||||
use bevy_core_pipeline::{
|
||||
core_3d::{AlphaMask3d, Opaque3d, Transparent3d},
|
||||
prepass::ViewPrepassTextures,
|
||||
|
@ -20,7 +20,6 @@ use bevy_ecs::{
|
|||
system::{lifetimeless::*, SystemParamItem, SystemState},
|
||||
};
|
||||
use bevy_math::{Affine3, Affine3A, Mat4, Vec2, Vec3Swizzles, Vec4};
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_render::{
|
||||
globals::{GlobalsBuffer, GlobalsUniform},
|
||||
mesh::{
|
||||
|
@ -57,24 +56,15 @@ pub const MAX_JOINTS: usize = 256;
|
|||
const JOINT_SIZE: usize = std::mem::size_of::<Mat4>();
|
||||
pub(crate) const JOINT_BUFFER_SIZE: usize = MAX_JOINTS * JOINT_SIZE;
|
||||
|
||||
pub const MESH_VERTEX_OUTPUT: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2645551199423808407);
|
||||
pub const MESH_VIEW_TYPES_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 8140454348013264787);
|
||||
pub const MESH_VIEW_BINDINGS_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 9076678235888822571);
|
||||
pub const MESH_TYPES_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 2506024101911992377);
|
||||
pub const MESH_BINDINGS_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 16831548636314682308);
|
||||
pub const MESH_FUNCTIONS_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 6300874327833745635);
|
||||
pub const MESH_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 3252377289100772450);
|
||||
pub const SKINNING_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 13215291596265391738);
|
||||
pub const MORPH_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 970982813587607345);
|
||||
pub const MESH_VERTEX_OUTPUT: Handle<Shader> = Handle::weak_from_u128(2645551199423808407);
|
||||
pub const MESH_VIEW_TYPES_HANDLE: Handle<Shader> = Handle::weak_from_u128(8140454348013264787);
|
||||
pub const MESH_VIEW_BINDINGS_HANDLE: Handle<Shader> = Handle::weak_from_u128(9076678235888822571);
|
||||
pub const MESH_TYPES_HANDLE: Handle<Shader> = Handle::weak_from_u128(2506024101911992377);
|
||||
pub const MESH_BINDINGS_HANDLE: Handle<Shader> = Handle::weak_from_u128(16831548636314682308);
|
||||
pub const MESH_FUNCTIONS_HANDLE: Handle<Shader> = Handle::weak_from_u128(6300874327833745635);
|
||||
pub const MESH_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(3252377289100772450);
|
||||
pub const SKINNING_HANDLE: Handle<Shader> = Handle::weak_from_u128(13215291596265391738);
|
||||
pub const MORPH_HANDLE: Handle<Shader> = Handle::weak_from_u128(970982813587607345);
|
||||
|
||||
impl Plugin for MeshRenderPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
|
@ -1025,13 +1015,13 @@ impl SpecializedMeshPipeline for MeshPipeline {
|
|||
|
||||
Ok(RenderPipelineDescriptor {
|
||||
vertex: VertexState {
|
||||
shader: MESH_SHADER_HANDLE.typed::<Shader>(),
|
||||
shader: MESH_SHADER_HANDLE,
|
||||
entry_point: "vertex".into(),
|
||||
shader_defs: shader_defs.clone(),
|
||||
buffers: vec![vertex_buffer_layout],
|
||||
},
|
||||
fragment: Some(FragmentState {
|
||||
shader: MESH_SHADER_HANDLE.typed::<Shader>(),
|
||||
shader: MESH_SHADER_HANDLE,
|
||||
shader_defs,
|
||||
entry_point: "fragment".into(),
|
||||
targets: vec![Some(ColorTargetState {
|
||||
|
@ -1082,7 +1072,7 @@ impl SpecializedMeshPipeline for MeshPipeline {
|
|||
pub struct MeshBindGroups {
|
||||
model_only: Option<BindGroup>,
|
||||
skinned: Option<BindGroup>,
|
||||
morph_targets: HashMap<HandleId, BindGroup>,
|
||||
morph_targets: HashMap<AssetId<Mesh>, BindGroup>,
|
||||
}
|
||||
impl MeshBindGroups {
|
||||
pub fn reset(&mut self) {
|
||||
|
@ -1091,7 +1081,12 @@ impl MeshBindGroups {
|
|||
self.morph_targets.clear();
|
||||
}
|
||||
/// Get the `BindGroup` for `GpuMesh` with given `handle_id`.
|
||||
pub fn get(&self, handle_id: HandleId, is_skinned: bool, morph: bool) -> Option<&BindGroup> {
|
||||
pub fn get(
|
||||
&self,
|
||||
handle_id: AssetId<Mesh>,
|
||||
is_skinned: bool,
|
||||
morph: bool,
|
||||
) -> Option<&BindGroup> {
|
||||
match (is_skinned, morph) {
|
||||
(_, true) => self.morph_targets.get(&handle_id),
|
||||
(true, false) => self.skinned.as_ref(),
|
||||
|
@ -1129,7 +1124,7 @@ pub fn prepare_mesh_bind_group(
|
|||
} else {
|
||||
layouts.morphed(&render_device, &model, weights, targets)
|
||||
};
|
||||
groups.morph_targets.insert(id.id(), group);
|
||||
groups.morph_targets.insert(id, group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{load_internal_asset, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_core_pipeline::{
|
||||
core_3d::CORE_3D,
|
||||
prelude::Camera3d,
|
||||
|
@ -13,7 +13,7 @@ use bevy_ecs::{
|
|||
system::{Commands, Query, Res, ResMut, Resource},
|
||||
world::{FromWorld, World},
|
||||
};
|
||||
use bevy_reflect::{Reflect, TypeUuid};
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_render::{
|
||||
camera::{ExtractedCamera, TemporalJitter},
|
||||
extract_component::ExtractComponent,
|
||||
|
@ -48,14 +48,10 @@ pub mod draw_3d_graph {
|
|||
}
|
||||
}
|
||||
|
||||
const PREPROCESS_DEPTH_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 102258915420479);
|
||||
const GTAO_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 253938746510568);
|
||||
const SPATIAL_DENOISE_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 466162052558226);
|
||||
const GTAO_UTILS_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 366465052568786);
|
||||
const PREPROCESS_DEPTH_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(102258915420479);
|
||||
const GTAO_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(253938746510568);
|
||||
const SPATIAL_DENOISE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(466162052558226);
|
||||
const GTAO_UTILS_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(366465052568786);
|
||||
|
||||
/// Plugin for screen space ambient occlusion.
|
||||
pub struct ScreenSpaceAmbientOcclusionPlugin;
|
||||
|
@ -537,7 +533,7 @@ impl FromWorld for SsaoPipelines {
|
|||
common_bind_group_layout.clone(),
|
||||
],
|
||||
push_constant_ranges: vec![],
|
||||
shader: PREPROCESS_DEPTH_SHADER_HANDLE.typed(),
|
||||
shader: PREPROCESS_DEPTH_SHADER_HANDLE,
|
||||
shader_defs: Vec::new(),
|
||||
entry_point: "preprocess_depth".into(),
|
||||
});
|
||||
|
@ -550,7 +546,7 @@ impl FromWorld for SsaoPipelines {
|
|||
common_bind_group_layout.clone(),
|
||||
],
|
||||
push_constant_ranges: vec![],
|
||||
shader: SPATIAL_DENOISE_SHADER_HANDLE.typed(),
|
||||
shader: SPATIAL_DENOISE_SHADER_HANDLE,
|
||||
shader_defs: Vec::new(),
|
||||
entry_point: "spatial_denoise".into(),
|
||||
});
|
||||
|
@ -601,7 +597,7 @@ impl SpecializedComputePipeline for SsaoPipelines {
|
|||
self.common_bind_group_layout.clone(),
|
||||
],
|
||||
push_constant_ranges: vec![],
|
||||
shader: GTAO_SHADER_HANDLE.typed(),
|
||||
shader: GTAO_SHADER_HANDLE,
|
||||
shader_defs,
|
||||
entry_point: "gtao".into(),
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use crate::{DrawMesh, MeshPipelineKey, SetMeshBindGroup, SetMeshViewBindGroup};
|
||||
use crate::{MeshPipeline, MeshTransforms};
|
||||
use bevy_app::Plugin;
|
||||
use bevy_asset::{load_internal_asset, Handle, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_core_pipeline::core_3d::Opaque3d;
|
||||
use bevy_ecs::{prelude::*, reflect::ReflectComponent};
|
||||
use bevy_reflect::std_traits::ReflectDefault;
|
||||
use bevy_reflect::{Reflect, TypeUuid};
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_render::extract_component::{ExtractComponent, ExtractComponentPlugin};
|
||||
use bevy_render::Render;
|
||||
use bevy_render::{
|
||||
|
@ -22,8 +22,7 @@ use bevy_render::{
|
|||
};
|
||||
use bevy_utils::tracing::error;
|
||||
|
||||
pub const WIREFRAME_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 192598014480025766);
|
||||
pub const WIREFRAME_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(192598014480025766);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WireframePlugin;
|
||||
|
@ -81,7 +80,7 @@ impl FromWorld for WireframePipeline {
|
|||
fn from_world(render_world: &mut World) -> Self {
|
||||
WireframePipeline {
|
||||
mesh_pipeline: render_world.resource::<MeshPipeline>().clone(),
|
||||
shader: WIREFRAME_SHADER_HANDLE.typed(),
|
||||
shader: WIREFRAME_SHADER_HANDLE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -199,6 +199,7 @@ impl_reflect_value!(::core::num::NonZeroI8(
|
|||
Serialize,
|
||||
Deserialize
|
||||
));
|
||||
impl_reflect_value!(::std::sync::Arc<T: Send + Sync>);
|
||||
|
||||
// `Serialize` and `Deserialize` only for platforms supported by serde:
|
||||
// https://github.com/serde-rs/serde/blob/3ffb86fc70efd3d329519e2dddfa306cc04f167c/serde/src/de/impls.rs#L1732
|
||||
|
|
|
@ -68,7 +68,6 @@ downcast-rs = "1.2.0"
|
|||
thread_local = "1.1"
|
||||
thiserror = "1.0"
|
||||
futures-lite = "1.4.0"
|
||||
anyhow = "1.0"
|
||||
hexasphere = "9.0"
|
||||
parking_lot = "0.12.1"
|
||||
ddsfile = { version = "0.5.0", optional = true }
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::{
|
|||
view::{ColorGrading, ExtractedView, ExtractedWindows, RenderLayers, VisibleEntities},
|
||||
Extract,
|
||||
};
|
||||
use bevy_asset::{AssetEvent, Assets, Handle};
|
||||
use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{
|
||||
change_detection::DetectChanges,
|
||||
|
@ -528,14 +528,14 @@ impl NormalizedRenderTarget {
|
|||
fn is_changed(
|
||||
&self,
|
||||
changed_window_ids: &HashSet<Entity>,
|
||||
changed_image_handles: &HashSet<&Handle<Image>>,
|
||||
changed_image_handles: &HashSet<&AssetId<Image>>,
|
||||
) -> bool {
|
||||
match self {
|
||||
NormalizedRenderTarget::Window(window_ref) => {
|
||||
changed_window_ids.contains(&window_ref.entity())
|
||||
}
|
||||
NormalizedRenderTarget::Image(image_handle) => {
|
||||
changed_image_handles.contains(&image_handle)
|
||||
changed_image_handles.contains(&image_handle.id())
|
||||
}
|
||||
NormalizedRenderTarget::TextureView(_) => true,
|
||||
}
|
||||
|
@ -578,11 +578,11 @@ pub fn camera_system<T: CameraProjection + Component>(
|
|||
changed_window_ids.extend(window_created_events.read().map(|event| event.window));
|
||||
changed_window_ids.extend(window_resized_events.read().map(|event| event.window));
|
||||
|
||||
let changed_image_handles: HashSet<&Handle<Image>> = image_asset_events
|
||||
let changed_image_handles: HashSet<&AssetId<Image>> = image_asset_events
|
||||
.read()
|
||||
.filter_map(|event| {
|
||||
if let AssetEvent::Modified { handle } = event {
|
||||
Some(handle)
|
||||
if let AssetEvent::Modified { id } = event {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
|
@ -6,14 +6,13 @@ use crate::{
|
|||
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
|
||||
};
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{load_internal_asset, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_core::FrameCount;
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_reflect::{Reflect, TypeUuid};
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_time::Time;
|
||||
|
||||
pub const GLOBALS_TYPE_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 17924628719070609599);
|
||||
pub const GLOBALS_TYPE_HANDLE: Handle<Shader> = Handle::weak_from_u128(17924628719070609599);
|
||||
|
||||
pub struct GlobalsPlugin;
|
||||
|
||||
|
|
|
@ -24,11 +24,6 @@ pub mod settings;
|
|||
mod spatial_bundle;
|
||||
pub mod texture;
|
||||
pub mod view;
|
||||
|
||||
use bevy_hierarchy::ValidParentCheckPlugin;
|
||||
use bevy_reflect::TypeUuid;
|
||||
pub use extract_param::Extract;
|
||||
|
||||
pub mod prelude {
|
||||
#[doc(hidden)]
|
||||
pub use crate::{
|
||||
|
@ -43,6 +38,9 @@ pub mod prelude {
|
|||
};
|
||||
}
|
||||
|
||||
pub use extract_param::Extract;
|
||||
|
||||
use bevy_hierarchy::ValidParentCheckPlugin;
|
||||
use bevy_window::{PrimaryWindow, RawHandleWrapper};
|
||||
use globals::GlobalsPlugin;
|
||||
use renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue};
|
||||
|
@ -58,7 +56,7 @@ use crate::{
|
|||
view::{ViewPlugin, WindowRenderPlugin},
|
||||
};
|
||||
use bevy_app::{App, AppLabel, Plugin, SubApp};
|
||||
use bevy_asset::{load_internal_asset, AddAsset, AssetServer, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, AssetApp, AssetServer, Handle};
|
||||
use bevy_ecs::{prelude::*, schedule::ScheduleLabel, system::SystemState};
|
||||
use bevy_utils::tracing::debug;
|
||||
use std::{
|
||||
|
@ -232,18 +230,15 @@ struct FutureRendererResources(
|
|||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, AppLabel)]
|
||||
pub struct RenderApp;
|
||||
|
||||
pub const INSTANCE_INDEX_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 10313207077636615845);
|
||||
pub const MATHS_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 10665356303104593376);
|
||||
pub const INSTANCE_INDEX_SHADER_HANDLE: Handle<Shader> =
|
||||
Handle::weak_from_u128(10313207077636615845);
|
||||
pub const MATHS_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(10665356303104593376);
|
||||
|
||||
impl Plugin for RenderPlugin {
|
||||
/// Initializes the renderer, sets up the [`RenderSet`](RenderSet) and creates the rendering sub-app.
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_asset::<Shader>()
|
||||
.add_debug_asset::<Shader>()
|
||||
.init_asset_loader::<ShaderLoader>()
|
||||
.init_debug_asset_loader::<ShaderLoader>();
|
||||
app.init_asset::<Shader>()
|
||||
.init_asset_loader::<ShaderLoader>();
|
||||
|
||||
if let Some(backends) = self.wgpu_settings.backends {
|
||||
let future_renderer_resources_wrapper = Arc::new(Mutex::new(None));
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
mod conversions;
|
||||
pub mod skinning;
|
||||
use bevy_log::warn;
|
||||
pub use wgpu::PrimitiveTopology;
|
||||
|
||||
use crate::{
|
||||
|
@ -10,12 +9,13 @@ use crate::{
|
|||
render_resource::{Buffer, TextureView, VertexBufferLayout},
|
||||
renderer::RenderDevice,
|
||||
};
|
||||
use bevy_asset::Handle;
|
||||
use bevy_asset::{Asset, Handle};
|
||||
use bevy_core::cast_slice;
|
||||
use bevy_derive::EnumVariantMeta;
|
||||
use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem};
|
||||
use bevy_log::warn;
|
||||
use bevy_math::*;
|
||||
use bevy_reflect::{TypePath, TypeUuid};
|
||||
use bevy_reflect::TypePath;
|
||||
use bevy_utils::{tracing::error, Hashed};
|
||||
use std::{collections::BTreeMap, hash::Hash, iter::FusedIterator};
|
||||
use thiserror::Error;
|
||||
|
@ -110,8 +110,7 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10;
|
|||
/// is the side of the triangle from where the vertices appear in a *counter-clockwise* order.
|
||||
///
|
||||
// TODO: allow values to be unloaded after been submitting to the GPU to conserve memory
|
||||
#[derive(Debug, TypeUuid, TypePath, Clone)]
|
||||
#[uuid = "8ecbac0f-f545-4473-ad43-e1f4243af51e"]
|
||||
#[derive(Asset, Debug, TypePath, Clone)]
|
||||
pub struct Mesh {
|
||||
primitive_topology: PrimitiveTopology,
|
||||
/// `std::collections::BTreeMap` with all defined vertex attributes (Positions, Normals, ...)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use bevy_asset::Handle;
|
||||
use bevy_asset::{Asset, Handle};
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
entity::{Entity, EntityMapper, MapEntities},
|
||||
|
@ -6,7 +6,7 @@ use bevy_ecs::{
|
|||
reflect::ReflectMapEntities,
|
||||
};
|
||||
use bevy_math::Mat4;
|
||||
use bevy_reflect::{Reflect, TypePath, TypeUuid};
|
||||
use bevy_reflect::{Reflect, TypePath};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[derive(Component, Debug, Default, Clone, Reflect)]
|
||||
|
@ -24,8 +24,7 @@ impl MapEntities for SkinnedMesh {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, TypeUuid, TypePath)]
|
||||
#[uuid = "b9f155a9-54ec-4026-988f-e0a03e99a76f"]
|
||||
#[derive(Asset, TypePath, Debug)]
|
||||
pub struct SkinnedMeshInverseBindposes(Box<[Mat4]>);
|
||||
|
||||
impl From<Vec<Mat4>> for SkinnedMeshInverseBindposes {
|
||||
|
|
|
@ -8,7 +8,7 @@ pub use mesh::*;
|
|||
|
||||
use crate::{prelude::Image, render_asset::RenderAssetPlugin};
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::AddAsset;
|
||||
use bevy_asset::AssetApp;
|
||||
use bevy_ecs::entity::Entity;
|
||||
|
||||
/// Adds the [`Mesh`] as an asset and makes sure that they are extracted and prepared for the GPU.
|
||||
|
@ -16,8 +16,8 @@ pub struct MeshPlugin;
|
|||
|
||||
impl Plugin for MeshPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_asset::<Mesh>()
|
||||
.add_asset::<skinning::SkinnedMeshInverseBindposes>()
|
||||
app.init_asset::<Mesh>()
|
||||
.init_asset::<skinning::SkinnedMeshInverseBindposes>()
|
||||
.register_type::<skinning::SkinnedMesh>()
|
||||
.register_type::<Vec<Entity>>()
|
||||
// 'Mesh' must be prepared after 'Image' as meshes rely on the morph target image being ready
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use crate::{Extract, ExtractSchedule, Render, RenderApp, RenderSet};
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{Asset, AssetEvent, Assets, Handle};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_asset::{Asset, AssetEvent, AssetId, Assets};
|
||||
use bevy_ecs::{
|
||||
prelude::*,
|
||||
schedule::SystemConfigs,
|
||||
|
@ -103,8 +102,8 @@ impl<A: RenderAsset> RenderAssetDependency for A {
|
|||
/// Temporarily stores the extracted and removed assets of the current frame.
|
||||
#[derive(Resource)]
|
||||
pub struct ExtractedAssets<A: RenderAsset> {
|
||||
extracted: Vec<(Handle<A>, A::ExtractedAsset)>,
|
||||
removed: Vec<Handle<A>>,
|
||||
extracted: Vec<(AssetId<A>, A::ExtractedAsset)>,
|
||||
removed: Vec<AssetId<A>>,
|
||||
}
|
||||
|
||||
impl<A: RenderAsset> Default for ExtractedAssets<A> {
|
||||
|
@ -118,8 +117,8 @@ impl<A: RenderAsset> Default for ExtractedAssets<A> {
|
|||
|
||||
/// Stores all GPU representations ([`RenderAsset::PreparedAssets`](RenderAsset::PreparedAsset))
|
||||
/// of [`RenderAssets`](RenderAsset) as long as they exist.
|
||||
#[derive(Resource, Deref, DerefMut)]
|
||||
pub struct RenderAssets<A: RenderAsset>(HashMap<Handle<A>, A::PreparedAsset>);
|
||||
#[derive(Resource)]
|
||||
pub struct RenderAssets<A: RenderAsset>(HashMap<AssetId<A>, A::PreparedAsset>);
|
||||
|
||||
impl<A: RenderAsset> Default for RenderAssets<A> {
|
||||
fn default() -> Self {
|
||||
|
@ -127,6 +126,36 @@ impl<A: RenderAsset> Default for RenderAssets<A> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<A: RenderAsset> RenderAssets<A> {
|
||||
pub fn get(&self, id: impl Into<AssetId<A>>) -> Option<&A::PreparedAsset> {
|
||||
self.0.get(&id.into())
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, id: impl Into<AssetId<A>>) -> Option<&mut A::PreparedAsset> {
|
||||
self.0.get_mut(&id.into())
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
id: impl Into<AssetId<A>>,
|
||||
value: A::PreparedAsset,
|
||||
) -> Option<A::PreparedAsset> {
|
||||
self.0.insert(id.into(), value)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: impl Into<AssetId<A>>) -> Option<A::PreparedAsset> {
|
||||
self.0.remove(&id.into())
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (AssetId<A>, &A::PreparedAsset)> {
|
||||
self.0.iter().map(|(k, v)| (*k, v))
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (AssetId<A>, &mut A::PreparedAsset)> {
|
||||
self.0.iter_mut().map(|(k, v)| (*k, v))
|
||||
}
|
||||
}
|
||||
|
||||
/// This system extracts all crated or modified assets of the corresponding [`RenderAsset`] type
|
||||
/// into the "render world".
|
||||
fn extract_render_asset<A: RenderAsset>(
|
||||
|
@ -138,20 +167,23 @@ fn extract_render_asset<A: RenderAsset>(
|
|||
let mut removed = Vec::new();
|
||||
for event in events.read() {
|
||||
match event {
|
||||
AssetEvent::Created { handle } | AssetEvent::Modified { handle } => {
|
||||
changed_assets.insert(handle.clone_weak());
|
||||
AssetEvent::Added { id } | AssetEvent::Modified { id } => {
|
||||
changed_assets.insert(*id);
|
||||
}
|
||||
AssetEvent::Removed { handle } => {
|
||||
changed_assets.remove(handle);
|
||||
removed.push(handle.clone_weak());
|
||||
AssetEvent::Removed { id } => {
|
||||
changed_assets.remove(id);
|
||||
removed.push(*id);
|
||||
}
|
||||
AssetEvent::LoadedWithDependencies { .. } => {
|
||||
// TODO: handle this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut extracted_assets = Vec::new();
|
||||
for handle in changed_assets.drain() {
|
||||
if let Some(asset) = assets.get(&handle) {
|
||||
extracted_assets.push((handle, asset.extract_asset()));
|
||||
for id in changed_assets.drain() {
|
||||
if let Some(asset) = assets.get(id) {
|
||||
extracted_assets.push((id, asset.extract_asset()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,7 +197,7 @@ fn extract_render_asset<A: RenderAsset>(
|
|||
/// All assets that should be prepared next frame.
|
||||
#[derive(Resource)]
|
||||
pub struct PrepareNextFrameAssets<A: RenderAsset> {
|
||||
assets: Vec<(Handle<A>, A::ExtractedAsset)>,
|
||||
assets: Vec<(AssetId<A>, A::ExtractedAsset)>,
|
||||
}
|
||||
|
||||
impl<A: RenderAsset> Default for PrepareNextFrameAssets<A> {
|
||||
|
@ -186,28 +218,28 @@ pub fn prepare_assets<R: RenderAsset>(
|
|||
) {
|
||||
let mut param = param.into_inner();
|
||||
let queued_assets = std::mem::take(&mut prepare_next_frame.assets);
|
||||
for (handle, extracted_asset) in queued_assets {
|
||||
for (id, extracted_asset) in queued_assets {
|
||||
match R::prepare_asset(extracted_asset, &mut param) {
|
||||
Ok(prepared_asset) => {
|
||||
render_assets.insert(handle, prepared_asset);
|
||||
render_assets.insert(id, prepared_asset);
|
||||
}
|
||||
Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
|
||||
prepare_next_frame.assets.push((handle, extracted_asset));
|
||||
prepare_next_frame.assets.push((id, extracted_asset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for removed in std::mem::take(&mut extracted_assets.removed) {
|
||||
render_assets.remove(&removed);
|
||||
render_assets.remove(removed);
|
||||
}
|
||||
|
||||
for (handle, extracted_asset) in std::mem::take(&mut extracted_assets.extracted) {
|
||||
for (id, extracted_asset) in std::mem::take(&mut extracted_assets.extracted) {
|
||||
match R::prepare_asset(extracted_asset, &mut param) {
|
||||
Ok(prepared_asset) => {
|
||||
render_assets.insert(handle, prepared_asset);
|
||||
render_assets.insert(id, prepared_asset);
|
||||
}
|
||||
Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
|
||||
prepare_next_frame.assets.push((handle, extracted_asset));
|
||||
prepare_next_frame.assets.push((id, extracted_asset));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::{
|
|||
renderer::RenderDevice,
|
||||
Extract,
|
||||
};
|
||||
use bevy_asset::{AssetEvent, Assets, Handle};
|
||||
use bevy_asset::{AssetEvent, AssetId, Assets};
|
||||
use bevy_ecs::system::{Res, ResMut};
|
||||
use bevy_ecs::{event::EventReader, system::Resource};
|
||||
use bevy_utils::{
|
||||
|
@ -121,15 +121,15 @@ impl CachedPipelineState {
|
|||
struct ShaderData {
|
||||
pipelines: HashSet<CachedPipelineId>,
|
||||
processed_shaders: HashMap<Vec<ShaderDefVal>, ErasedShaderModule>,
|
||||
resolved_imports: HashMap<ShaderImport, Handle<Shader>>,
|
||||
dependents: HashSet<Handle<Shader>>,
|
||||
resolved_imports: HashMap<ShaderImport, AssetId<Shader>>,
|
||||
dependents: HashSet<AssetId<Shader>>,
|
||||
}
|
||||
|
||||
struct ShaderCache {
|
||||
data: HashMap<Handle<Shader>, ShaderData>,
|
||||
shaders: HashMap<Handle<Shader>, Shader>,
|
||||
import_path_shaders: HashMap<ShaderImport, Handle<Shader>>,
|
||||
waiting_on_import: HashMap<ShaderImport, Vec<Handle<Shader>>>,
|
||||
data: HashMap<AssetId<Shader>, ShaderData>,
|
||||
shaders: HashMap<AssetId<Shader>, Shader>,
|
||||
import_path_shaders: HashMap<ShaderImport, AssetId<Shader>>,
|
||||
waiting_on_import: HashMap<ShaderImport, Vec<AssetId<Shader>>>,
|
||||
composer: naga_oil::compose::Composer,
|
||||
}
|
||||
|
||||
|
@ -210,8 +210,8 @@ impl ShaderCache {
|
|||
|
||||
fn add_import_to_composer(
|
||||
composer: &mut naga_oil::compose::Composer,
|
||||
import_path_shaders: &HashMap<ShaderImport, Handle<Shader>>,
|
||||
shaders: &HashMap<Handle<Shader>, Shader>,
|
||||
import_path_shaders: &HashMap<ShaderImport, AssetId<Shader>>,
|
||||
shaders: &HashMap<AssetId<Shader>, Shader>,
|
||||
import: &ShaderImport,
|
||||
) -> Result<(), PipelineCacheError> {
|
||||
if !composer.contains_module(&import.module_name()) {
|
||||
|
@ -240,14 +240,14 @@ impl ShaderCache {
|
|||
&mut self,
|
||||
render_device: &RenderDevice,
|
||||
pipeline: CachedPipelineId,
|
||||
handle: &Handle<Shader>,
|
||||
id: AssetId<Shader>,
|
||||
shader_defs: &[ShaderDefVal],
|
||||
) -> Result<ErasedShaderModule, PipelineCacheError> {
|
||||
let shader = self
|
||||
.shaders
|
||||
.get(handle)
|
||||
.ok_or_else(|| PipelineCacheError::ShaderNotLoaded(handle.clone_weak()))?;
|
||||
let data = self.data.entry(handle.clone_weak()).or_default();
|
||||
.get(&id)
|
||||
.ok_or(PipelineCacheError::ShaderNotLoaded(id))?;
|
||||
let data = self.data.entry(id).or_default();
|
||||
let n_asset_imports = shader
|
||||
.imports()
|
||||
.filter(|import| matches!(import, ShaderImport::AssetPath(_)))
|
||||
|
@ -281,7 +281,7 @@ impl ShaderCache {
|
|||
|
||||
debug!(
|
||||
"processing shader {:?}, with shader defs {:?}",
|
||||
handle, shader_defs
|
||||
id, shader_defs
|
||||
);
|
||||
let shader_source = match &shader.source {
|
||||
#[cfg(feature = "shader_format_spirv")]
|
||||
|
@ -357,14 +357,14 @@ impl ShaderCache {
|
|||
Ok(module.clone())
|
||||
}
|
||||
|
||||
fn clear(&mut self, handle: &Handle<Shader>) -> Vec<CachedPipelineId> {
|
||||
let mut shaders_to_clear = vec![handle.clone_weak()];
|
||||
fn clear(&mut self, id: AssetId<Shader>) -> Vec<CachedPipelineId> {
|
||||
let mut shaders_to_clear = vec![id];
|
||||
let mut pipelines_to_queue = Vec::new();
|
||||
while let Some(handle) = shaders_to_clear.pop() {
|
||||
if let Some(data) = self.data.get_mut(&handle) {
|
||||
data.processed_shaders.clear();
|
||||
pipelines_to_queue.extend(data.pipelines.iter().cloned());
|
||||
shaders_to_clear.extend(data.dependents.iter().map(|h| h.clone_weak()));
|
||||
pipelines_to_queue.extend(data.pipelines.iter().copied());
|
||||
shaders_to_clear.extend(data.dependents.iter().copied());
|
||||
|
||||
if let Some(Shader { import_path, .. }) = self.shaders.get(&handle) {
|
||||
self.composer
|
||||
|
@ -376,45 +376,42 @@ impl ShaderCache {
|
|||
pipelines_to_queue
|
||||
}
|
||||
|
||||
fn set_shader(&mut self, handle: &Handle<Shader>, shader: Shader) -> Vec<CachedPipelineId> {
|
||||
let pipelines_to_queue = self.clear(handle);
|
||||
fn set_shader(&mut self, id: AssetId<Shader>, shader: Shader) -> Vec<CachedPipelineId> {
|
||||
let pipelines_to_queue = self.clear(id);
|
||||
let path = shader.import_path();
|
||||
self.import_path_shaders
|
||||
.insert(path.clone(), handle.clone_weak());
|
||||
self.import_path_shaders.insert(path.clone(), id);
|
||||
if let Some(waiting_shaders) = self.waiting_on_import.get_mut(path) {
|
||||
for waiting_shader in waiting_shaders.drain(..) {
|
||||
// resolve waiting shader import
|
||||
let data = self.data.entry(waiting_shader.clone_weak()).or_default();
|
||||
data.resolved_imports
|
||||
.insert(path.clone(), handle.clone_weak());
|
||||
let data = self.data.entry(waiting_shader).or_default();
|
||||
data.resolved_imports.insert(path.clone(), id);
|
||||
// add waiting shader as dependent of this shader
|
||||
let data = self.data.entry(handle.clone_weak()).or_default();
|
||||
data.dependents.insert(waiting_shader.clone_weak());
|
||||
let data = self.data.entry(id).or_default();
|
||||
data.dependents.insert(waiting_shader);
|
||||
}
|
||||
}
|
||||
|
||||
for import in shader.imports() {
|
||||
if let Some(import_handle) = self.import_path_shaders.get(import) {
|
||||
if let Some(import_id) = self.import_path_shaders.get(import).copied() {
|
||||
// resolve import because it is currently available
|
||||
let data = self.data.entry(handle.clone_weak()).or_default();
|
||||
data.resolved_imports
|
||||
.insert(import.clone(), import_handle.clone_weak());
|
||||
let data = self.data.entry(id).or_default();
|
||||
data.resolved_imports.insert(import.clone(), import_id);
|
||||
// add this shader as a dependent of the import
|
||||
let data = self.data.entry(import_handle.clone_weak()).or_default();
|
||||
data.dependents.insert(handle.clone_weak());
|
||||
let data = self.data.entry(import_id).or_default();
|
||||
data.dependents.insert(id);
|
||||
} else {
|
||||
let waiting = self.waiting_on_import.entry(import.clone()).or_default();
|
||||
waiting.push(handle.clone_weak());
|
||||
waiting.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
self.shaders.insert(handle.clone_weak(), shader);
|
||||
self.shaders.insert(id, shader);
|
||||
pipelines_to_queue
|
||||
}
|
||||
|
||||
fn remove(&mut self, handle: &Handle<Shader>) -> Vec<CachedPipelineId> {
|
||||
let pipelines_to_queue = self.clear(handle);
|
||||
if let Some(shader) = self.shaders.remove(handle) {
|
||||
fn remove(&mut self, id: AssetId<Shader>) -> Vec<CachedPipelineId> {
|
||||
let pipelines_to_queue = self.clear(id);
|
||||
if let Some(shader) = self.shaders.remove(&id) {
|
||||
self.import_path_shaders.remove(shader.import_path());
|
||||
}
|
||||
|
||||
|
@ -625,15 +622,15 @@ impl PipelineCache {
|
|||
id
|
||||
}
|
||||
|
||||
fn set_shader(&mut self, handle: &Handle<Shader>, shader: &Shader) {
|
||||
let pipelines_to_queue = self.shader_cache.set_shader(handle, shader.clone());
|
||||
fn set_shader(&mut self, id: AssetId<Shader>, shader: &Shader) {
|
||||
let pipelines_to_queue = self.shader_cache.set_shader(id, shader.clone());
|
||||
for cached_pipeline in pipelines_to_queue {
|
||||
self.pipelines[cached_pipeline].state = CachedPipelineState::Queued;
|
||||
self.waiting_pipelines.insert(cached_pipeline);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_shader(&mut self, shader: &Handle<Shader>) {
|
||||
fn remove_shader(&mut self, shader: AssetId<Shader>) {
|
||||
let pipelines_to_queue = self.shader_cache.remove(shader);
|
||||
for cached_pipeline in pipelines_to_queue {
|
||||
self.pipelines[cached_pipeline].state = CachedPipelineState::Queued;
|
||||
|
@ -649,7 +646,7 @@ impl PipelineCache {
|
|||
let vertex_module = match self.shader_cache.get(
|
||||
&self.device,
|
||||
id,
|
||||
&descriptor.vertex.shader,
|
||||
descriptor.vertex.shader.id(),
|
||||
&descriptor.vertex.shader_defs,
|
||||
) {
|
||||
Ok(module) => module,
|
||||
|
@ -662,7 +659,7 @@ impl PipelineCache {
|
|||
let fragment_module = match self.shader_cache.get(
|
||||
&self.device,
|
||||
id,
|
||||
&fragment.shader,
|
||||
fragment.shader.id(),
|
||||
&fragment.shader_defs,
|
||||
) {
|
||||
Ok(module) => module,
|
||||
|
@ -734,7 +731,7 @@ impl PipelineCache {
|
|||
let compute_module = match self.shader_cache.get(
|
||||
&self.device,
|
||||
id,
|
||||
&descriptor.shader,
|
||||
descriptor.shader.id(),
|
||||
&descriptor.shader_defs,
|
||||
) {
|
||||
Ok(module) => module,
|
||||
|
@ -834,12 +831,15 @@ impl PipelineCache {
|
|||
) {
|
||||
for event in events.read() {
|
||||
match event {
|
||||
AssetEvent::Created { handle } | AssetEvent::Modified { handle } => {
|
||||
if let Some(shader) = shaders.get(handle) {
|
||||
cache.set_shader(handle, shader);
|
||||
AssetEvent::Added { id } | AssetEvent::Modified { id } => {
|
||||
if let Some(shader) = shaders.get(*id) {
|
||||
cache.set_shader(*id, shader);
|
||||
}
|
||||
}
|
||||
AssetEvent::Removed { handle } => cache.remove_shader(handle),
|
||||
AssetEvent::Removed { id } => cache.remove_shader(*id),
|
||||
AssetEvent::LoadedWithDependencies { .. } => {
|
||||
// TODO: handle this
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -851,7 +851,7 @@ pub enum PipelineCacheError {
|
|||
#[error(
|
||||
"Pipeline could not be compiled because the following shader is not loaded yet: {0:?}"
|
||||
)]
|
||||
ShaderNotLoaded(Handle<Shader>),
|
||||
ShaderNotLoaded(AssetId<Shader>),
|
||||
#[error(transparent)]
|
||||
ProcessShaderError(#[from] naga_oil::compose::ComposerError),
|
||||
#[error("Shader import not yet available.")]
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use super::ShaderDefVal;
|
||||
use crate::define_atomic_id;
|
||||
use bevy_asset::{AssetLoader, AssetPath, Handle, LoadContext, LoadedAsset};
|
||||
use bevy_reflect::{TypePath, TypeUuid};
|
||||
use bevy_asset::{anyhow, io::Reader, Asset, AssetLoader, AssetPath, Handle, LoadContext};
|
||||
use bevy_reflect::TypePath;
|
||||
use bevy_utils::{tracing::error, BoxedFuture};
|
||||
|
||||
use futures_lite::AsyncReadExt;
|
||||
use std::{borrow::Cow, marker::Copy};
|
||||
use thiserror::Error;
|
||||
|
||||
|
@ -24,8 +24,7 @@ pub enum ShaderReflectError {
|
|||
}
|
||||
/// A shader, as defined by its [`ShaderSource`](wgpu::ShaderSource) and [`ShaderStage`](naga::ShaderStage)
|
||||
/// This is an "unprocessed" shader. It can contain preprocessor directives.
|
||||
#[derive(Debug, Clone, TypeUuid, TypePath)]
|
||||
#[uuid = "d95bc916-6c55-4de3-9622-37e7b6969fda"]
|
||||
#[derive(Asset, TypePath, Debug, Clone)]
|
||||
pub struct Shader {
|
||||
pub path: String,
|
||||
pub source: Source,
|
||||
|
@ -234,34 +233,37 @@ impl From<&Source> for naga_oil::compose::ShaderType {
|
|||
pub struct ShaderLoader;
|
||||
|
||||
impl AssetLoader for ShaderLoader {
|
||||
type Asset = Shader;
|
||||
type Settings = ();
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
bytes: &'a [u8],
|
||||
reader: &'a mut Reader,
|
||||
_settings: &'a Self::Settings,
|
||||
load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, Result<(), anyhow::Error>> {
|
||||
) -> BoxedFuture<'a, Result<Shader, anyhow::Error>> {
|
||||
Box::pin(async move {
|
||||
let ext = load_context.path().extension().unwrap().to_str().unwrap();
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
reader.read_to_end(&mut bytes).await?;
|
||||
let shader = match ext {
|
||||
"spv" => {
|
||||
Shader::from_spirv(Vec::from(bytes), load_context.path().to_string_lossy())
|
||||
}
|
||||
"spv" => Shader::from_spirv(bytes, load_context.path().to_string_lossy()),
|
||||
"wgsl" => Shader::from_wgsl(
|
||||
String::from_utf8(Vec::from(bytes))?,
|
||||
String::from_utf8(bytes)?,
|
||||
load_context.path().to_string_lossy(),
|
||||
),
|
||||
"vert" => Shader::from_glsl(
|
||||
String::from_utf8(Vec::from(bytes))?,
|
||||
String::from_utf8(bytes)?,
|
||||
naga::ShaderStage::Vertex,
|
||||
load_context.path().to_string_lossy(),
|
||||
),
|
||||
"frag" => Shader::from_glsl(
|
||||
String::from_utf8(Vec::from(bytes))?,
|
||||
String::from_utf8(bytes)?,
|
||||
naga::ShaderStage::Fragment,
|
||||
load_context.path().to_string_lossy(),
|
||||
),
|
||||
"comp" => Shader::from_glsl(
|
||||
String::from_utf8(Vec::from(bytes))?,
|
||||
String::from_utf8(bytes)?,
|
||||
naga::ShaderStage::Compute,
|
||||
load_context.path().to_string_lossy(),
|
||||
),
|
||||
|
@ -269,25 +271,13 @@ impl AssetLoader for ShaderLoader {
|
|||
};
|
||||
|
||||
// collect file dependencies
|
||||
let dependencies = shader
|
||||
.imports
|
||||
.iter()
|
||||
.flat_map(|import| {
|
||||
if let ShaderImport::AssetPath(asset_path) = import {
|
||||
Some(asset_path.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut asset = LoadedAsset::new(shader);
|
||||
for dependency in dependencies {
|
||||
asset.add_dependency(dependency.into());
|
||||
for import in shader.imports.iter() {
|
||||
if let ShaderImport::AssetPath(asset_path) = import {
|
||||
// TODO: should we just allow this handle to be dropped?
|
||||
let _handle: Handle<Shader> = load_context.load(asset_path);
|
||||
}
|
||||
}
|
||||
|
||||
load_context.set_default_asset(asset);
|
||||
Ok(())
|
||||
Ok(shader)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -101,7 +101,8 @@ pub fn basis_buffer_to_image(
|
|||
width: image0_info.m_orig_width,
|
||||
height: image0_info.m_orig_height,
|
||||
depth_or_array_layers: image_count,
|
||||
};
|
||||
}
|
||||
.physical_size(texture_format);
|
||||
image.texture_descriptor.mip_level_count = image0_mip_level_count;
|
||||
image.texture_descriptor.format = texture_format;
|
||||
image.texture_descriptor.dimension = match texture_type {
|
||||
|
|
56
crates/bevy_render/src/texture/compressed_image_saver.rs
Normal file
56
crates/bevy_render/src/texture/compressed_image_saver.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use crate::texture::{Image, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderSettings};
|
||||
use bevy_asset::{
|
||||
anyhow::Error,
|
||||
saver::{AssetSaver, SavedAsset},
|
||||
};
|
||||
use futures_lite::{AsyncWriteExt, FutureExt};
|
||||
|
||||
pub struct CompressedImageSaver;
|
||||
|
||||
impl AssetSaver for CompressedImageSaver {
|
||||
type Asset = Image;
|
||||
|
||||
type Settings = ();
|
||||
type OutputLoader = ImageLoader;
|
||||
|
||||
fn save<'a>(
|
||||
&'a self,
|
||||
writer: &'a mut bevy_asset::io::Writer,
|
||||
image: SavedAsset<'a, Self::Asset>,
|
||||
_settings: &'a Self::Settings,
|
||||
) -> bevy_utils::BoxedFuture<'a, std::result::Result<ImageLoaderSettings, Error>> {
|
||||
// PERF: this should live inside the future, but CompressorParams and Compressor are not Send / can't be owned by the BoxedFuture (which _is_ Send)
|
||||
let mut compressor_params = basis_universal::CompressorParams::new();
|
||||
compressor_params.set_basis_format(basis_universal::BasisTextureFormat::UASTC4x4);
|
||||
compressor_params.set_generate_mipmaps(true);
|
||||
let is_srgb = image.texture_descriptor.format.is_srgb();
|
||||
let color_space = if is_srgb {
|
||||
basis_universal::ColorSpace::Srgb
|
||||
} else {
|
||||
basis_universal::ColorSpace::Linear
|
||||
};
|
||||
compressor_params.set_color_space(color_space);
|
||||
compressor_params.set_uastc_quality_level(basis_universal::UASTC_QUALITY_DEFAULT);
|
||||
|
||||
let mut source_image = compressor_params.source_image_mut(0);
|
||||
let size = image.size();
|
||||
source_image.init(&image.data, size.x as u32, size.y as u32, 4);
|
||||
|
||||
let mut compressor = basis_universal::Compressor::new(4);
|
||||
// SAFETY: the CompressorParams are "valid" to the best of our knowledge. The basis-universal
|
||||
// library bindings note that invalid params might produce undefined behavior.
|
||||
unsafe {
|
||||
compressor.init(&compressor_params);
|
||||
compressor.process().unwrap();
|
||||
}
|
||||
let compressed_basis_data = compressor.basis_file().to_vec();
|
||||
async move {
|
||||
writer.write_all(&compressed_basis_data).await?;
|
||||
Ok(ImageLoaderSettings {
|
||||
format: ImageFormatSetting::Format(ImageFormat::Basis),
|
||||
is_srgb,
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
use crate::texture::{Image, TextureFormatPixelInfo};
|
||||
use anyhow::Result;
|
||||
use bevy_asset::{AssetLoader, LoadContext, LoadedAsset};
|
||||
use bevy_asset::{
|
||||
anyhow::Error,
|
||||
io::{AsyncReadExt, Reader},
|
||||
AssetLoader, LoadContext,
|
||||
};
|
||||
use bevy_utils::BoxedFuture;
|
||||
use image::ImageDecoder;
|
||||
use wgpu::{Extent3d, TextureDimension, TextureFormat};
|
||||
|
@ -10,11 +13,15 @@ use wgpu::{Extent3d, TextureDimension, TextureFormat};
|
|||
pub struct ExrTextureLoader;
|
||||
|
||||
impl AssetLoader for ExrTextureLoader {
|
||||
type Asset = Image;
|
||||
type Settings = ();
|
||||
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
bytes: &'a [u8],
|
||||
load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, Result<()>> {
|
||||
reader: &'a mut Reader,
|
||||
_settings: &'a Self::Settings,
|
||||
_load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, Result<Image, Error>> {
|
||||
Box::pin(async move {
|
||||
let format = TextureFormat::Rgba32Float;
|
||||
debug_assert_eq!(
|
||||
|
@ -23,6 +30,8 @@ impl AssetLoader for ExrTextureLoader {
|
|||
"Format should have 32bit x 4 size"
|
||||
);
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
reader.read_to_end(&mut bytes).await?;
|
||||
let decoder = image::codecs::openexr::OpenExrDecoder::with_alpha_preference(
|
||||
std::io::Cursor::new(bytes),
|
||||
Some(true),
|
||||
|
@ -34,7 +43,7 @@ impl AssetLoader for ExrTextureLoader {
|
|||
let mut buf = vec![0u8; total_bytes];
|
||||
decoder.read_image(buf.as_mut_slice())?;
|
||||
|
||||
let texture = Image::new(
|
||||
Ok(Image::new(
|
||||
Extent3d {
|
||||
width,
|
||||
height,
|
||||
|
@ -43,10 +52,7 @@ impl AssetLoader for ExrTextureLoader {
|
|||
TextureDimension::D2,
|
||||
buf,
|
||||
format,
|
||||
);
|
||||
|
||||
load_context.set_default_asset(LoadedAsset::new(texture));
|
||||
Ok(())
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use crate::texture::{Image, TextureFormatPixelInfo};
|
||||
use anyhow::Result;
|
||||
use bevy_asset::{AssetLoader, LoadContext, LoadedAsset};
|
||||
use bevy_utils::BoxedFuture;
|
||||
use bevy_asset::{anyhow::Error, io::Reader, AssetLoader, AsyncReadExt, LoadContext};
|
||||
use wgpu::{Extent3d, TextureDimension, TextureFormat};
|
||||
|
||||
/// Loads HDR textures as Texture assets
|
||||
|
@ -9,11 +7,14 @@ use wgpu::{Extent3d, TextureDimension, TextureFormat};
|
|||
pub struct HdrTextureLoader;
|
||||
|
||||
impl AssetLoader for HdrTextureLoader {
|
||||
type Asset = Image;
|
||||
type Settings = ();
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
bytes: &'a [u8],
|
||||
load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, Result<()>> {
|
||||
reader: &'a mut Reader,
|
||||
_settings: &'a (),
|
||||
_load_context: &'a mut LoadContext,
|
||||
) -> bevy_utils::BoxedFuture<'a, Result<Image, Error>> {
|
||||
Box::pin(async move {
|
||||
let format = TextureFormat::Rgba32Float;
|
||||
debug_assert_eq!(
|
||||
|
@ -22,7 +23,9 @@ impl AssetLoader for HdrTextureLoader {
|
|||
"Format should have 32bit x 4 size"
|
||||
);
|
||||
|
||||
let decoder = image::codecs::hdr::HdrDecoder::new(bytes)?;
|
||||
let mut bytes = Vec::new();
|
||||
reader.read_to_end(&mut bytes).await?;
|
||||
let decoder = image::codecs::hdr::HdrDecoder::new(bytes.as_slice())?;
|
||||
let info = decoder.metadata();
|
||||
let rgb_data = decoder.read_image_hdr()?;
|
||||
let mut rgba_data = Vec::with_capacity(rgb_data.len() * format.pixel_size());
|
||||
|
@ -36,7 +39,7 @@ impl AssetLoader for HdrTextureLoader {
|
|||
rgba_data.extend_from_slice(&alpha.to_ne_bytes());
|
||||
}
|
||||
|
||||
let texture = Image::new(
|
||||
Ok(Image::new(
|
||||
Extent3d {
|
||||
width: info.width,
|
||||
height: info.height,
|
||||
|
@ -45,10 +48,7 @@ impl AssetLoader for HdrTextureLoader {
|
|||
TextureDimension::D2,
|
||||
rgba_data,
|
||||
format,
|
||||
);
|
||||
|
||||
load_context.set_default_asset(LoadedAsset::new(texture));
|
||||
Ok(())
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -11,22 +11,20 @@ use crate::{
|
|||
renderer::{RenderDevice, RenderQueue},
|
||||
texture::BevyDefault,
|
||||
};
|
||||
use bevy_asset::HandleUntyped;
|
||||
use bevy_asset::Asset;
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::system::{lifetimeless::SRes, Resource, SystemParamItem};
|
||||
use bevy_math::Vec2;
|
||||
use bevy_reflect::{Reflect, TypeUuid};
|
||||
|
||||
use bevy_reflect::Reflect;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::hash::Hash;
|
||||
use thiserror::Error;
|
||||
use wgpu::{Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor};
|
||||
|
||||
pub const TEXTURE_ASSET_INDEX: u64 = 0;
|
||||
pub const SAMPLER_ASSET_INDEX: u64 = 1;
|
||||
pub const DEFAULT_IMAGE_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Image::TYPE_UUID, 13148262314052771789);
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
pub enum ImageFormat {
|
||||
Avif,
|
||||
Basis,
|
||||
|
@ -103,8 +101,7 @@ impl ImageFormat {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Reflect, Debug, Clone, TypeUuid)]
|
||||
#[uuid = "6ea26da6-6cf8-4ea2-9986-1d7bf6c17d6f"]
|
||||
#[derive(Asset, Reflect, Debug, Clone)]
|
||||
#[reflect_value]
|
||||
pub struct Image {
|
||||
pub data: Vec<u8>,
|
||||
|
@ -444,11 +441,14 @@ pub enum TextureError {
|
|||
}
|
||||
|
||||
/// The type of a raw image buffer.
|
||||
#[derive(Debug)]
|
||||
pub enum ImageType<'a> {
|
||||
/// The mime type of an image, for example `"image/png"`.
|
||||
MimeType(&'a str),
|
||||
/// The extension of an image file, for example `"png"`.
|
||||
Extension(&'a str),
|
||||
/// The direct format of the image
|
||||
Format(ImageFormat),
|
||||
}
|
||||
|
||||
impl<'a> ImageType<'a> {
|
||||
|
@ -458,6 +458,7 @@ impl<'a> ImageType<'a> {
|
|||
.ok_or_else(|| TextureError::InvalidImageMimeType(mime_type.to_string())),
|
||||
ImageType::Extension(extension) => ImageFormat::from_extension(extension)
|
||||
.ok_or_else(|| TextureError::InvalidImageExtension(extension.to_string())),
|
||||
ImageType::Format(format) => Ok(*format),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
use anyhow::Result;
|
||||
use bevy_asset::{AssetLoader, LoadContext, LoadedAsset};
|
||||
use bevy_asset::{anyhow, io::Reader, AssetLoader, AsyncReadExt, LoadContext};
|
||||
use bevy_ecs::prelude::{FromWorld, World};
|
||||
use bevy_utils::BoxedFuture;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
renderer::RenderDevice,
|
||||
texture::{Image, ImageType, TextureError},
|
||||
texture::{Image, ImageFormat, ImageType, TextureError},
|
||||
};
|
||||
|
||||
use super::CompressedImageFormats;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Loader for images that can be read by the `image` crate.
|
||||
#[derive(Clone)]
|
||||
pub struct ImageTextureLoader {
|
||||
pub struct ImageLoader {
|
||||
supported_compressed_formats: CompressedImageFormats,
|
||||
}
|
||||
|
||||
|
@ -46,29 +46,57 @@ pub(crate) const IMG_FILE_EXTENSIONS: &[&str] = &[
|
|||
"ppm",
|
||||
];
|
||||
|
||||
impl AssetLoader for ImageTextureLoader {
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub enum ImageFormatSetting {
|
||||
#[default]
|
||||
FromExtension,
|
||||
Format(ImageFormat),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ImageLoaderSettings {
|
||||
pub format: ImageFormatSetting,
|
||||
pub is_srgb: bool,
|
||||
}
|
||||
|
||||
impl Default for ImageLoaderSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
format: ImageFormatSetting::default(),
|
||||
is_srgb: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AssetLoader for ImageLoader {
|
||||
type Asset = Image;
|
||||
type Settings = ImageLoaderSettings;
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
bytes: &'a [u8],
|
||||
reader: &'a mut Reader,
|
||||
settings: &'a ImageLoaderSettings,
|
||||
load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, Result<()>> {
|
||||
) -> bevy_utils::BoxedFuture<'a, Result<Image, anyhow::Error>> {
|
||||
Box::pin(async move {
|
||||
// use the file extension for the image type
|
||||
let ext = load_context.path().extension().unwrap().to_str().unwrap();
|
||||
|
||||
let dyn_img = Image::from_buffer(
|
||||
bytes,
|
||||
ImageType::Extension(ext),
|
||||
let mut bytes = Vec::new();
|
||||
reader.read_to_end(&mut bytes).await?;
|
||||
let image_type = match settings.format {
|
||||
ImageFormatSetting::FromExtension => ImageType::Extension(ext),
|
||||
ImageFormatSetting::Format(format) => ImageType::Format(format),
|
||||
};
|
||||
Ok(Image::from_buffer(
|
||||
&bytes,
|
||||
image_type,
|
||||
self.supported_compressed_formats,
|
||||
true,
|
||||
settings.is_srgb,
|
||||
)
|
||||
.map_err(|err| FileTextureError {
|
||||
error: err,
|
||||
path: format!("{}", load_context.path().display()),
|
||||
})?;
|
||||
|
||||
load_context.set_default_asset(LoadedAsset::new(dyn_img));
|
||||
Ok(())
|
||||
})?)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -77,7 +105,7 @@ impl AssetLoader for ImageTextureLoader {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromWorld for ImageTextureLoader {
|
||||
impl FromWorld for ImageLoader {
|
||||
fn from_world(world: &mut World) -> Self {
|
||||
let supported_compressed_formats = match world.get_resource::<RenderDevice>() {
|
||||
Some(render_device) => CompressedImageFormats::from_features(render_device.features()),
|
|
@ -1,5 +1,5 @@
|
|||
use crate::texture::{Image, TextureFormatPixelInfo};
|
||||
use anyhow::anyhow;
|
||||
use bevy_asset::anyhow;
|
||||
use image::{DynamicImage, ImageBuffer};
|
||||
use wgpu::{Extent3d, TextureDimension, TextureFormat};
|
||||
|
||||
|
@ -163,7 +163,7 @@ impl Image {
|
|||
/// - `TextureFormat::Bgra8UnormSrgb`
|
||||
///
|
||||
/// To convert [`Image`] to a different format see: [`Image::convert`].
|
||||
pub fn try_into_dynamic(self) -> anyhow::Result<DynamicImage> {
|
||||
pub fn try_into_dynamic(self) -> Result<DynamicImage, anyhow::Error> {
|
||||
match self.texture_descriptor.format {
|
||||
TextureFormat::R8Unorm => ImageBuffer::from_raw(
|
||||
self.texture_descriptor.size.width,
|
||||
|
@ -199,14 +199,14 @@ impl Image {
|
|||
.map(DynamicImage::ImageRgba8),
|
||||
// Throw and error if conversion isn't supported
|
||||
texture_format => {
|
||||
return Err(anyhow!(
|
||||
return Err(anyhow::anyhow!(
|
||||
"Conversion into dynamic image not supported for {:?}.",
|
||||
texture_format
|
||||
))
|
||||
}
|
||||
}
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
anyhow::anyhow!(
|
||||
"Failed to convert into {:?}.",
|
||||
self.texture_descriptor.format
|
||||
)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#[cfg(feature = "basis-universal")]
|
||||
mod basis;
|
||||
#[cfg(feature = "basis-universal")]
|
||||
mod compressed_image_saver;
|
||||
#[cfg(feature = "dds")]
|
||||
mod dds;
|
||||
#[cfg(feature = "exr")]
|
||||
|
@ -9,7 +11,7 @@ mod fallback_image;
|
|||
mod hdr_texture_loader;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod image;
|
||||
mod image_texture_loader;
|
||||
mod image_loader;
|
||||
#[cfg(feature = "ktx2")]
|
||||
mod ktx2;
|
||||
mod texture_cache;
|
||||
|
@ -26,15 +28,17 @@ pub use exr_texture_loader::*;
|
|||
#[cfg(feature = "hdr")]
|
||||
pub use hdr_texture_loader::*;
|
||||
|
||||
#[cfg(feature = "basis-universal")]
|
||||
pub use compressed_image_saver::*;
|
||||
pub use fallback_image::*;
|
||||
pub use image_texture_loader::*;
|
||||
pub use image_loader::*;
|
||||
pub use texture_cache::*;
|
||||
|
||||
use crate::{
|
||||
render_asset::RenderAssetPlugin, renderer::RenderDevice, Render, RenderApp, RenderSet,
|
||||
};
|
||||
use bevy_app::{App, Plugin};
|
||||
use bevy_asset::{AddAsset, Assets};
|
||||
use bevy_asset::{AssetApp, Assets, Handle};
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
// TODO: replace Texture names with Image names?
|
||||
|
@ -80,11 +84,22 @@ impl Plugin for ImagePlugin {
|
|||
|
||||
app.add_plugins(RenderAssetPlugin::<Image>::default())
|
||||
.register_type::<Image>()
|
||||
.add_asset::<Image>()
|
||||
.init_asset::<Image>()
|
||||
.register_asset_reflect::<Image>();
|
||||
app.world
|
||||
.resource_mut::<Assets<Image>>()
|
||||
.set_untracked(DEFAULT_IMAGE_HANDLE, Image::default());
|
||||
.insert(Handle::default(), Image::default());
|
||||
#[cfg(feature = "basis-universal")]
|
||||
if let Some(processor) = app
|
||||
.world
|
||||
.get_resource::<bevy_asset::processor::AssetProcessor>()
|
||||
{
|
||||
processor.register_processor::<bevy_asset::processor::LoadAndSave<ImageLoader, CompressedImageSaver>>(
|
||||
CompressedImageSaver.into(),
|
||||
);
|
||||
processor
|
||||
.set_default_processor::<bevy_asset::processor::LoadAndSave<ImageLoader, CompressedImageSaver>>("png");
|
||||
}
|
||||
|
||||
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
render_app.init_resource::<TextureCache>().add_systems(
|
||||
|
@ -102,7 +117,7 @@ impl Plugin for ImagePlugin {
|
|||
feature = "basis-universal",
|
||||
feature = "ktx2",
|
||||
))]
|
||||
app.preregister_asset_loader(IMG_FILE_EXTENSIONS);
|
||||
app.preregister_asset_loader::<ImageLoader>(IMG_FILE_EXTENSIONS);
|
||||
}
|
||||
|
||||
fn finish(&self, app: &mut App) {
|
||||
|
@ -116,7 +131,7 @@ impl Plugin for ImagePlugin {
|
|||
feature = "ktx2",
|
||||
))]
|
||||
{
|
||||
app.init_asset_loader::<ImageTextureLoader>();
|
||||
app.init_asset_loader::<ImageLoader>();
|
||||
}
|
||||
|
||||
if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
pub mod visibility;
|
||||
pub mod window;
|
||||
|
||||
use bevy_asset::{load_internal_asset, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
pub use visibility::*;
|
||||
pub use window::*;
|
||||
|
||||
|
@ -19,7 +19,7 @@ use crate::{
|
|||
use bevy_app::{App, Plugin};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_math::{Mat4, UVec4, Vec3, Vec4, Vec4Swizzles};
|
||||
use bevy_reflect::{Reflect, TypeUuid};
|
||||
use bevy_reflect::Reflect;
|
||||
use bevy_transform::components::GlobalTransform;
|
||||
use bevy_utils::HashMap;
|
||||
use std::sync::{
|
||||
|
@ -31,8 +31,7 @@ use wgpu::{
|
|||
TextureFormat, TextureUsages,
|
||||
};
|
||||
|
||||
pub const VIEW_TYPE_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 15421373904451797197);
|
||||
pub const VIEW_TYPE_HANDLE: Handle<Shader> = Handle::weak_from_u128(15421373904451797197);
|
||||
|
||||
pub struct ViewPlugin;
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
use std::{borrow::Cow, path::Path};
|
||||
|
||||
use bevy_app::Plugin;
|
||||
use bevy_asset::{load_internal_asset, HandleUntyped};
|
||||
use bevy_asset::{load_internal_asset, Handle};
|
||||
use bevy_ecs::prelude::*;
|
||||
use bevy_log::{error, info, info_span};
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_tasks::AsyncComputeTaskPool;
|
||||
use bevy_utils::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
|
@ -122,8 +121,7 @@ impl ScreenshotManager {
|
|||
|
||||
pub struct ScreenshotPlugin;
|
||||
|
||||
const SCREENSHOT_SHADER_HANDLE: HandleUntyped =
|
||||
HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 11918575842344596158);
|
||||
const SCREENSHOT_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(11918575842344596158);
|
||||
|
||||
impl Plugin for ScreenshotPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
|
@ -231,7 +229,7 @@ impl SpecializedRenderPipeline for ScreenshotToScreenPipeline {
|
|||
buffers: vec![],
|
||||
shader_defs: vec![],
|
||||
entry_point: Cow::Borrowed("vs_main"),
|
||||
shader: SCREENSHOT_SHADER_HANDLE.typed(),
|
||||
shader: SCREENSHOT_SHADER_HANDLE,
|
||||
},
|
||||
primitive: wgpu::PrimitiveState {
|
||||
cull_mode: Some(wgpu::Face::Back),
|
||||
|
@ -240,7 +238,7 @@ impl SpecializedRenderPipeline for ScreenshotToScreenPipeline {
|
|||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
fragment: Some(FragmentState {
|
||||
shader: SCREENSHOT_SHADER_HANDLE.typed(),
|
||||
shader: SCREENSHOT_SHADER_HANDLE,
|
||||
entry_point: Cow::Borrowed("fs_main"),
|
||||
shader_defs: vec![],
|
||||
targets: vec![Some(wgpu::ColorTargetState {
|
||||
|
|
|
@ -28,7 +28,6 @@ bevy_render = { path = "../bevy_render", version = "0.12.0-dev", optional = true
|
|||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
ron = "0.8.0"
|
||||
uuid = { version = "1.1", features = ["v4"] }
|
||||
anyhow = "1.0.4"
|
||||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
use std::any::TypeId;
|
||||
|
||||
use crate::{DynamicSceneBuilder, Scene, SceneSpawnError};
|
||||
use anyhow::Result;
|
||||
use bevy_ecs::{
|
||||
entity::Entity,
|
||||
reflect::{AppTypeRegistry, ReflectComponent, ReflectMapEntities},
|
||||
world::World,
|
||||
};
|
||||
use bevy_reflect::{Reflect, TypePath, TypeRegistryArc, TypeUuid};
|
||||
use bevy_reflect::{Reflect, TypePath, TypeRegistryArc};
|
||||
use bevy_utils::HashMap;
|
||||
use std::any::TypeId;
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
use crate::serde::SceneSerializer;
|
||||
use bevy_asset::Asset;
|
||||
use bevy_ecs::reflect::ReflectResource;
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::Serialize;
|
||||
|
@ -25,8 +24,7 @@ use serde::Serialize;
|
|||
/// * adding the [`Handle<DynamicScene>`](bevy_asset::Handle) to an entity (the scene will only be
|
||||
/// visible if the entity already has [`Transform`](bevy_transform::components::Transform) and
|
||||
/// [`GlobalTransform`](bevy_transform::components::GlobalTransform) components)
|
||||
#[derive(Default, TypeUuid, TypePath)]
|
||||
#[uuid = "749479b1-fb8c-4ff8-a775-623aa76014f5"]
|
||||
#[derive(Asset, TypePath, Default)]
|
||||
pub struct DynamicScene {
|
||||
pub resources: Vec<Box<dyn Reflect>>,
|
||||
pub entities: Vec<DynamicEntity>,
|
||||
|
|
|
@ -28,8 +28,8 @@ pub mod prelude {
|
|||
};
|
||||
}
|
||||
|
||||
use bevy_app::{prelude::*, SpawnScene};
|
||||
use bevy_asset::AddAsset;
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::AssetApp;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ScenePlugin;
|
||||
|
@ -37,8 +37,8 @@ pub struct ScenePlugin;
|
|||
#[cfg(feature = "serialize")]
|
||||
impl Plugin for ScenePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_asset::<DynamicScene>()
|
||||
.add_asset::<Scene>()
|
||||
app.init_asset::<DynamicScene>()
|
||||
.init_asset::<Scene>()
|
||||
.init_asset_loader::<SceneLoader>()
|
||||
.add_event::<SceneInstanceReady>()
|
||||
.init_resource::<SceneSpawner>()
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
use crate::{DynamicScene, InstanceInfo, SceneSpawnError};
|
||||
use bevy_asset::Asset;
|
||||
use bevy_ecs::{
|
||||
reflect::{AppTypeRegistry, ReflectComponent, ReflectMapEntities, ReflectResource},
|
||||
world::World,
|
||||
};
|
||||
use bevy_reflect::{TypePath, TypeUuid};
|
||||
use bevy_reflect::TypePath;
|
||||
use bevy_utils::HashMap;
|
||||
|
||||
use crate::{DynamicScene, InstanceInfo, SceneSpawnError};
|
||||
|
||||
/// To spawn a scene, you can use either:
|
||||
/// * [`SceneSpawner::spawn`](crate::SceneSpawner::spawn)
|
||||
/// * adding the [`SceneBundle`](crate::SceneBundle) to an entity
|
||||
/// * adding the [`Handle<Scene>`](bevy_asset::Handle) to an entity (the scene will only be
|
||||
/// visible if the entity already has [`Transform`](bevy_transform::components::Transform) and
|
||||
/// [`GlobalTransform`](bevy_transform::components::GlobalTransform) components)
|
||||
#[derive(Debug, TypeUuid, TypePath)]
|
||||
#[uuid = "c156503c-edd9-4ec7-8d33-dab392df03cd"]
|
||||
#[derive(Asset, TypePath, Debug)]
|
||||
pub struct Scene {
|
||||
pub world: World,
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
#[cfg(feature = "serialize")]
|
||||
use crate::serde::SceneDeserializer;
|
||||
use anyhow::{anyhow, Result};
|
||||
use bevy_asset::{AssetLoader, LoadContext, LoadedAsset};
|
||||
use crate::DynamicScene;
|
||||
use bevy_asset::{anyhow, io::Reader, AssetLoader, AsyncReadExt, LoadContext};
|
||||
use bevy_ecs::reflect::AppTypeRegistry;
|
||||
use bevy_ecs::world::{FromWorld, World};
|
||||
use bevy_reflect::TypeRegistryArc;
|
||||
use bevy_utils::BoxedFuture;
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::de::DeserializeSeed;
|
||||
|
||||
|
@ -26,29 +25,33 @@ impl FromWorld for SceneLoader {
|
|||
|
||||
#[cfg(feature = "serialize")]
|
||||
impl AssetLoader for SceneLoader {
|
||||
type Asset = DynamicScene;
|
||||
type Settings = ();
|
||||
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
bytes: &'a [u8],
|
||||
reader: &'a mut Reader,
|
||||
_settings: &'a (),
|
||||
load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, Result<()>> {
|
||||
) -> BoxedFuture<'a, Result<Self::Asset, anyhow::Error>> {
|
||||
Box::pin(async move {
|
||||
let mut deserializer = ron::de::Deserializer::from_bytes(bytes)?;
|
||||
let mut bytes = Vec::new();
|
||||
reader.read_to_end(&mut bytes).await?;
|
||||
let mut deserializer = ron::de::Deserializer::from_bytes(&bytes)?;
|
||||
let scene_deserializer = SceneDeserializer {
|
||||
type_registry: &self.type_registry.read(),
|
||||
};
|
||||
let scene = scene_deserializer
|
||||
scene_deserializer
|
||||
.deserialize(&mut deserializer)
|
||||
.map_err(|e| {
|
||||
let span_error = deserializer.span_error(e);
|
||||
anyhow!(
|
||||
anyhow::anyhow!(
|
||||
"{} at {}:{}",
|
||||
span_error.code,
|
||||
load_context.path().to_string_lossy(),
|
||||
span_error.position,
|
||||
)
|
||||
})?;
|
||||
load_context.set_default_asset(LoadedAsset::new(scene));
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue