Introduction
Welcome to Keket
!
Modern, flexible, modular Asset Management library built on top of ECS as its storage.
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::{bundle::BundleAssetProtocol, bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use serde::Deserialize;
use serde_json::Value;
use std::{error::Error, fs::Metadata, path::PathBuf};
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Person {
age: u8,
home: PersonHome,
friends: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct PersonHome {
country: String,
address: String,
}
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Asset protocols tell how to deserialize bytes into assets.
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
// Bundle protocol allows for easly making a protocol that takes
// bytes and returns bundle with optional dependencies.
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Value>(&bytes)?;
Ok((asset,).into())
}))
.with_protocol(BundleAssetProtocol::new("person", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Person>(&bytes)?;
Ok((asset,).into())
}))
// Asset fetch tells how to get bytes from specific source.
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure method either gives existing asset handle or creates new
// and loads asset if not existing yet in storage.
let lorem = database.ensure("text://lorem.txt")?;
// Accessing component(s) of asset entry.
// Assets can store multiple data associated to them, consider them meta data.
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
let json = database.ensure("json://person.json")?;
println!("JSON: {:#}", json.access::<&Value>(&database));
let person = database.ensure("person://person.json")?;
println!("Person: {:#?}", person.access::<&Person>(&database));
let trash = database.ensure("bytes://trash.bin")?;
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
// We can query storage for asset components to process assets, just like with ECS.
for (asset_path, file_path, metadata) in database
.storage
.query::<true, (&AssetPath, &PathBuf, &Metadata)>()
{
println!(
"Asset: `{}` at location: {:?} has metadata: {:#?}",
asset_path, file_path, metadata
);
}
Ok(())
}
Goal
Keket
started as an experiment to tell how asset management would look like
with ECS as its storage, how we could make it play with modularity, what benefits
does ECS storage gives us.
Soon after first version got done, i've realized that it is quite powerful and easily extendable, when we treat assets as entities with components as their data (and meta data) and asset loaders (and systems outside of asset management) can process and change assets in bulk the way they need, while not forcing any particular closed specs structure on the assets themselves.
Asset Database
Let's dive into central point of the library - asset database.
AssetDatabase
is a thin, higher level abstraction for underlying (public) ECS
storage where assets data live.
ECS as storage allowed to treat assets as rows in database tables, where single asset can have multiple data columns associated with given asset. This opened up a possibility for fast queries and lookups on multiple entities that pass given requirements (set of components).
This is very important for systems which process and modify assets in specific
for them ways (in Keket
those systems are asset fetch engines and asset
protocols - more about them later in the book).
We can use queries and lookups to process only assets that have given set of
requirements based on the data they store. For example when we load assets with
FileAssetFetch
, we get not only asset data, but also meta data as PathBuf
and
Metadata
components, so if we want to for example scan database for all assets
that come from file system, we can just query them with PathBuf
component and
use that fast query for asset statistics report.
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::{bundle::BundleAssetProtocol, bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use serde::Deserialize;
use serde_json::Value;
use std::{error::Error, fs::Metadata, path::PathBuf};
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Person {
age: u8,
home: PersonHome,
friends: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct PersonHome {
country: String,
address: String,
}
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Asset protocols tell how to deserialize bytes into assets.
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
// Bundle protocol allows for easly making a protocol that takes
// bytes and returns bundle with optional dependencies.
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Value>(&bytes)?;
Ok((asset,).into())
}))
.with_protocol(BundleAssetProtocol::new("person", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Person>(&bytes)?;
Ok((asset,).into())
}))
// Asset fetch tells how to get bytes from specific source.
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure method either gives existing asset handle or creates new
// and loads asset if not existing yet in storage.
let lorem = database.ensure("text://lorem.txt")?;
// Accessing component(s) of asset entry.
// Assets can store multiple data associated to them, consider them meta data.
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
let json = database.ensure("json://person.json")?;
println!("JSON: {:#}", json.access::<&Value>(&database));
let person = database.ensure("person://person.json")?;
println!("Person: {:#?}", person.access::<&Person>(&database));
let trash = database.ensure("bytes://trash.bin")?;
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
// We can query storage for asset components to process assets, just like with ECS.
for (asset_path, file_path, metadata) in database
.storage
.query::<true, (&AssetPath, &PathBuf, &Metadata)>()
{
println!(
"Asset: `{}` at location: {:?} has metadata: {:#?}",
asset_path, file_path, metadata
);
}
Ok(())
}
Since storage is just an ECS, every asset entity always must store at least
AssetPath
component to be considered valid asset - every component other than
asset path is considered asset data that is either usable for outside systems,
or for asset fetch engines and asset protocols as meta data for them in asset
resolution process.
When an asset entity stores some components without asset path, then it is never considered an actual asset, and rather an entity not important to asset management - some systems might find it useful for them, but it is highly not advised to put there anything other than assets.
Asset life cycle
There is standarized progression in asset life cycle:
- Awaits for resolution - database triggers fetching bytes for these.
- Bytes are ready to be processed - database takes those loaded bytes from asset and runs asset protocol (by asset path protocol part) on these bytes.
- Asset is ready to use - bytes are decoded into asset components and put in given asset for use by outside systems.
- Asset is unloaded - asset entity gets despawned and when ensured again it will be marked as awaiting resolution.
It might be the case where sometimes one or more of these steps are missed, for example when asset is added manually (user has all asset components made and spawns asset directly into storage, without fetching bytes, etc) - in that case given asset never goes through asset fetch engines and asset protocols (yet is still discoverable by change detection).
Asset Handle
AssetHandle
is a thin wrapper around ECS entity type (generational index).
Its used as key to asset in database, to allow said database to perform operations
on the asset using that handle - handles are returned when asset gets spawned in
storage, so every spawned handle always point to existing asset (unless asset gets
unloaded/deleted).
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::{bundle::BundleAssetProtocol, bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use serde::Deserialize;
use serde_json::Value;
use std::{error::Error, fs::Metadata, path::PathBuf};
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Person {
age: u8,
home: PersonHome,
friends: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct PersonHome {
country: String,
address: String,
}
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Asset protocols tell how to deserialize bytes into assets.
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
// Bundle protocol allows for easly making a protocol that takes
// bytes and returns bundle with optional dependencies.
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Value>(&bytes)?;
Ok((asset,).into())
}))
.with_protocol(BundleAssetProtocol::new("person", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Person>(&bytes)?;
Ok((asset,).into())
}))
// Asset fetch tells how to get bytes from specific source.
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure method either gives existing asset handle or creates new
// and loads asset if not existing yet in storage.
let lorem = database.ensure("text://lorem.txt")?;
// Accessing component(s) of asset entry.
// Assets can store multiple data associated to them, consider them meta data.
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
let json = database.ensure("json://person.json")?;
println!("JSON: {:#}", json.access::<&Value>(&database));
let person = database.ensure("person://person.json")?;
println!("Person: {:#?}", person.access::<&Person>(&database));
let trash = database.ensure("bytes://trash.bin")?;
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
// We can query storage for asset components to process assets, just like with ECS.
for (asset_path, file_path, metadata) in database
.storage
.query::<true, (&AssetPath, &PathBuf, &Metadata)>()
{
println!(
"Asset: `{}` at location: {:?} has metadata: {:#?}",
asset_path, file_path, metadata
);
}
Ok(())
}
Asset Path
AssetPath
serves as thin wrapper around copy-on-write string with cached ranges
of standarized path (<protocol>://<path>?<key1>=<value1>&<flag>
) components:
- protocol
- path
- optional meta data
Example asset paths:
ui/texts/lorem.txt
- asset path without protocol, only path part.text://ui/texts/lorem.txt
- asset path with protocol.text://ui/texts/lorem.txt?v=3&uppercase
- asset path with path and meta key-values.
Most common asset paths are ones with protocol and meta key-values. Paths without protocols are possible, but not useful, unless you register an asset protocol that handles them (which is almost never the case, but one could have it as protocol resolved by any other means - since library is modular, user can setup database however they like).
Asset paths are useful mostly in getting asset handles by their asset paths, or storage queries/lookups by specific asset path.
let path = AssetPath::new("text://lorem.txt?v=3&uppercase");
println!("- protocol: {:?}", path.protocol());
for segment in path.path_parts() {
println!(" - path segment: {:?}", segment);
}
for (key, value) in path.meta_items() {
println!(" - path meta key: {:?} value: {:?}", key, value);
}
let lorem = database.ensure(path)?;
Asset Reference
AssetRef
is a thin wrapper around asset path and optional cached asset handle.
The goal here was to allow to reduce resolving asset handles to one resolution for given asset reference - searching for asset handle by asset path is unnecessary work to do everytime we point to an asset by path and need to perform operations on its handle, so with asset reference we can resolve asset handle once and reuse its cached handle for future database operations.
Additional justification for asset references is serialization - this is preferred way to reference assets in serializable data, so once container gets deserializaed into typed data, asset handle resolution will happen only at first time.
use keket::{
database::{reference::AssetRef, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::bundle::{
BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor,
},
};
use serde::Deserialize;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
.with_protocol(BundleAssetProtocol::new("custom", CustomAssetProcessor))
.with_fetch(FileAssetFetch::default().with_root("resources"));
let handle = database.ensure("custom://part1.json")?;
while database.is_busy() {
database.maintain()?;
}
let contents = handle.access::<&CustomAsset>(&database).contents(&database);
println!("Custom chain contents: {:?}", contents);
Ok(())
}
#[derive(Debug, Default, Deserialize)]
struct CustomAsset {
content: String,
// Asset references are used to store path and cached handle. They serialize as asset paths.
// They can be used where we need to have an ability to reference asset by path, and ask
// for handle once instead of everytime as with asset paths.
#[serde(default)]
next: Option<AssetRef>,
}
impl CustomAsset {
fn contents(&self, database: &AssetDatabase) -> String {
let mut result = self.content.as_str().to_owned();
if let Some(next) = self.next.as_ref() {
result.push(' ');
if let Ok(resolved) = next.resolve(database) {
result.push_str(&resolved.access::<&Self>().contents(database));
}
}
result
}
}
struct CustomAssetProcessor;
impl BundleWithDependenciesProcessor for CustomAssetProcessor {
type Bundle = (CustomAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<CustomAsset>(&bytes)?;
let dependency = asset
.next
.as_ref()
.map(|reference| reference.path().clone());
Ok(BundleWithDependencies::new((asset,)).maybe_dependency(dependency))
}
}
use keket::{
database::{reference::AssetRef, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::bundle::{
BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor,
},
};
use serde::Deserialize;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
.with_protocol(BundleAssetProtocol::new("custom", CustomAssetProcessor))
.with_fetch(FileAssetFetch::default().with_root("resources"));
let handle = database.ensure("custom://part1.json")?;
while database.is_busy() {
database.maintain()?;
}
let contents = handle.access::<&CustomAsset>(&database).contents(&database);
println!("Custom chain contents: {:?}", contents);
Ok(())
}
#[derive(Debug, Default, Deserialize)]
struct CustomAsset {
content: String,
// Asset references are used to store path and cached handle. They serialize as asset paths.
// They can be used where we need to have an ability to reference asset by path, and ask
// for handle once instead of everytime as with asset paths.
#[serde(default)]
next: Option<AssetRef>,
}
impl CustomAsset {
fn contents(&self, database: &AssetDatabase) -> String {
let mut result = self.content.as_str().to_owned();
if let Some(next) = self.next.as_ref() {
result.push(' ');
if let Ok(resolved) = next.resolve(database) {
result.push_str(&resolved.access::<&Self>().contents(database));
}
}
result
}
}
struct CustomAssetProcessor;
impl BundleWithDependenciesProcessor for CustomAssetProcessor {
type Bundle = (CustomAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<CustomAsset>(&bytes)?;
let dependency = asset
.next
.as_ref()
.map(|reference| reference.path().clone());
Ok(BundleWithDependencies::new((asset,)).maybe_dependency(dependency))
}
}
Asset Events
AssetEventBindings
is a container for event listeners that will be notified
about particular asset (or all assets) progression.
User can put asset event bindings into particular asset, then some code can bind
its listener (anything that implements AssetEventListener
trait, for example
closure or Sender
), and once asset progression changes, that will be dispatched
to registered listeners. User can also listen for all asset events by registering
to event bindings in asset database.
use keket::{
database::{events::AssetEventBindings, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::{
bundle::BundleAssetProtocol, bytes::BytesAssetProtocol, group::GroupAssetProtocol,
text::TextAssetProtocol,
},
};
use serde_json::Value;
use std::{error::Error, sync::mpsc::channel};
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Value>(&bytes)?;
Ok((asset,).into())
}))
.with_protocol(GroupAssetProtocol)
.with_fetch(FileAssetFetch::default().with_root("resources"));
// We can bind closures to asset event bindings for any asset progression tracking.
database.events.bind(|event| {
println!("Asset closure event: {:#?}", event);
Ok(())
});
// Create channel for asset events communication.
let (tx, rx) = channel();
// Start loading asset and its dependencies.
let group = database.ensure("group://group.txt")?;
// We can also bind sender to asset event bindings.
group.ensure::<AssetEventBindings>(&mut database)?.bind(tx);
while database.is_busy() {
database.maintain()?;
}
// Read sent events from receiver.
while let Ok(event) = rx.try_recv() {
println!("Group channel event: {:#?}", event);
}
println!(
"Lorem Ipsum: {}",
database
.find("text://lorem.txt")
.unwrap()
.access::<&String>(&database)
);
Ok(())
}
Asset Fetch
Asset fetch engines are units that implement AssetFetch
trait that tells asset
manager how and where from to get requested asset bytes. This covers first step
of asset progression, where next step uses asset protocol to decode loaded bytes.
Usually what we can see in asset management libraries is asset loaders, which are combination of loading bytes (from some specific to them source) and decoding bytes into asset object. I've found this approach unnecessarily fixed and forcing requirement for either implementing same asset loader for every asset source, or to make all possible asset sources implemented in asset loader.
In Keket
i went with decoupling bytes loading from bytes decoding, so that user
can for example use different bytes source for different build mode or different
platform, without having to bytes decoding to care about where bytes come from.
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::{bundle::BundleAssetProtocol, bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use serde::Deserialize;
use serde_json::Value;
use std::{error::Error, fs::Metadata, path::PathBuf};
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Person {
age: u8,
home: PersonHome,
friends: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct PersonHome {
country: String,
address: String,
}
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Asset protocols tell how to deserialize bytes into assets.
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
// Bundle protocol allows for easly making a protocol that takes
// bytes and returns bundle with optional dependencies.
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Value>(&bytes)?;
Ok((asset,).into())
}))
.with_protocol(BundleAssetProtocol::new("person", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Person>(&bytes)?;
Ok((asset,).into())
}))
// Asset fetch tells how to get bytes from specific source.
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure method either gives existing asset handle or creates new
// and loads asset if not existing yet in storage.
let lorem = database.ensure("text://lorem.txt")?;
// Accessing component(s) of asset entry.
// Assets can store multiple data associated to them, consider them meta data.
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
let json = database.ensure("json://person.json")?;
println!("JSON: {:#}", json.access::<&Value>(&database));
let person = database.ensure("person://person.json")?;
println!("Person: {:#?}", person.access::<&Person>(&database));
let trash = database.ensure("bytes://trash.bin")?;
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
// We can query storage for asset components to process assets, just like with ECS.
for (asset_path, file_path, metadata) in database
.storage
.query::<true, (&AssetPath, &PathBuf, &Metadata)>()
{
println!(
"Asset: `{}` at location: {:?} has metadata: {:#?}",
asset_path, file_path, metadata
);
}
Ok(())
}
let mut database = AssetDatabase::default()
.with_protocol(TextAssetProtocol)
// Whichever asset source we use, we will make them async load.
.with_fetch(DeferredAssetFetch::new(
// Hot-reloading assets from file system for development.
#[cfg(not(feature = "shipping"))]
HotReloadFileAssetFetch::new(
FileAssetFetch::default().with_root("assets"),
Duration::from_secs(5),
)?
// Loading assets from asset pack REDB database.
#[cfg(feature = "shipping")]
ContainerAssetFetch::new(RedbContainerPartialFetch::new(
Database::create("./assets.redb")?,
"assets",
))
));
let handle = database.ensure("text://lorem.txt")?;
println!("Lorem Ipsum: {}", handle.access::<&String>(&database));
And this is how easy it is to implement new asset fetch engine:
impl AssetFetch for FileAssetFetch {
fn load_bytes(&self, path: AssetPath) -> Result<DynamicBundle, Box<dyn Error>> {
let file_path = self.root.join(path.path());
let bytes = std::fs::read(&file_path)
.map_err(|error| format!("Failed to load `{:?}` file bytes: {}", file_path, error))?;
let mut bundle = DynamicBundle::default();
bundle
.add_component(AssetBytesAreReadyToProcess(bytes))
.ok()
.unwrap();
bundle.add_component(AssetFromFile).ok().unwrap();
bundle
.add_component(std::fs::metadata(&file_path)?)
.ok()
.unwrap();
bundle.add_component(file_path).ok().unwrap();
Ok(bundle)
}
}
AssetBytesAreReadyToProcess
component is crucial in asset progression, because it marks asset for database (and outside systems) as loaded but not yet decoded, so database can detect these assets and trigger asset decoding with their protocol.
File asset fetch also adds other components such as:
AssetFromFile
- tag component that allows to query all assets from file system.std::fs::Metadata
- file metadata that can be used for example for size stats.PathBuf
- file system path that can be used for stuff like hot reloading.
Asset fetch engines other than FileassetFetch
also do add their own custom
metadata to asset.
It's worth also to know that asset database uses stack of asset fetch engines, to allow changing source of assets for particular code paths - we can push, pop, swap or use-in-place fetch engines. Typical scenario of that would be to procedurally create an asset container that we can then use as container source for future assets.
File system
FileAssetFetch
is a simple fetch engine that reads entire bytes of an asset
file synchronously.
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::{bundle::BundleAssetProtocol, bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use serde::Deserialize;
use serde_json::Value;
use std::{error::Error, fs::Metadata, path::PathBuf};
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Person {
age: u8,
home: PersonHome,
friends: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct PersonHome {
country: String,
address: String,
}
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Asset protocols tell how to deserialize bytes into assets.
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
// Bundle protocol allows for easly making a protocol that takes
// bytes and returns bundle with optional dependencies.
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Value>(&bytes)?;
Ok((asset,).into())
}))
.with_protocol(BundleAssetProtocol::new("person", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Person>(&bytes)?;
Ok((asset,).into())
}))
// Asset fetch tells how to get bytes from specific source.
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure method either gives existing asset handle or creates new
// and loads asset if not existing yet in storage.
let lorem = database.ensure("text://lorem.txt")?;
// Accessing component(s) of asset entry.
// Assets can store multiple data associated to them, consider them meta data.
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
let json = database.ensure("json://person.json")?;
println!("JSON: {:#}", json.access::<&Value>(&database));
let person = database.ensure("person://person.json")?;
println!("Person: {:#?}", person.access::<&Person>(&database));
let trash = database.ensure("bytes://trash.bin")?;
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
// We can query storage for asset components to process assets, just like with ECS.
for (asset_path, file_path, metadata) in database
.storage
.query::<true, (&AssetPath, &PathBuf, &Metadata)>()
{
println!(
"Asset: `{}` at location: {:?} has metadata: {:#?}",
asset_path, file_path, metadata
);
}
Ok(())
}
Since this is blocking fetch, you might want to wrap it with
DeferredAssetFetch
to run file system fetching jobs in background.
In-memory collections
In rare cases we need to have assets in-memory - embeded platforms, or just assets baked into binary (typically simple web wasm games) - we can use collections to store paths and their bytes and allow assets to be fetched from there.
const ASSETS: &[(&str, &[u8])] = &[
("lorem.txt", include_bytes!("./assets/lorem.txt")),
("trash.bin", include_bytes!("./assets/trash.bin")),
];
let mut database = AssetDatabase::default()
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
.with_fetch(ASSETS);
let lorem = database.ensure("text://lorem.txt")?;
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
let trash = database.ensure("bytes://trash.bin")?;
println!("Trash: {:?}", bytes.access::<&Vec<u8>>(&database));
Hot-reloading wrapper for file system
HotReloadAssetFetch
wrapper alows assets loaded from FileAssetFetch
to be
watched and reloaded when source file changes.
use keket::{
database::{handle::AssetHandle, path::AssetPath, AssetDatabase},
fetch::{file::FileAssetFetch, hotreload::HotReloadFileAssetFetch},
protocol::{bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use std::{error::Error, time::Duration};
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
// Hot reload wrapper watches for changes in file fetch root path.
.with_fetch(HotReloadFileAssetFetch::new(
FileAssetFetch::default().with_root("resources"),
// File system watcher polling interval.
Duration::from_secs(5),
)?);
// First we fill database with some assets, hot reload only
// cares about changes in files present in database.
database.ensure("text://lorem.txt")?;
database.ensure("bytes://trash.bin")?;
println!("Watching for file changes...");
loop {
database.maintain()?;
// With storage change detection we can ask for asset entities
// that their paths were updated (hot reloading updates them).
if let Some(changes) = database.storage.updated() {
for entity in changes.iter_of::<AssetPath>() {
println!(
"Asset changed: `{}`",
AssetHandle::new(entity).access::<&AssetPath>(&database)
);
}
}
// Simulate systems that detect particular asset type reload.
for entity in database.storage.added().iter_of::<String>() {
println!(
"Text asset changed: `{}`",
AssetHandle::new(entity).access::<&String>(&database)
);
}
for entity in database.storage.added().iter_of::<Vec<u8>>() {
println!(
"Bytes asset changed: `{:?}`",
AssetHandle::new(entity).access::<&Vec<u8>>(&database)
);
}
}
}
Deferred loading wrapper
DeferredAssetFetch
allows to asynchronously load requested bytes in background
thread(s). It is encouraged to use it for heavy latency fetch engines like file
system or network fetch engines.
use keket::{
database::AssetDatabase,
fetch::{deferred::DeferredAssetFetch, file::FileAssetFetch},
protocol::bytes::BytesAssetProtocol,
};
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
.with_protocol(BytesAssetProtocol)
// Deferred asset fetch runs fetching jobs in threads for any fetch engine.
.with_fetch(DeferredAssetFetch::new(
FileAssetFetch::default().with_root("resources"),
));
let package = database.ensure("bytes://package.zip")?;
// Simulate waiting for asset bytes loading to complete.
while package.awaits_deferred_job(&database) {
println!("Package awaits deferred job done");
database.maintain()?;
}
// Run another maintain pass to process loaded bytes.
database.maintain()?;
println!(
"Package byte size: {}",
package.access::<&Vec<u8>>(&database).len()
);
Ok(())
}
Assets container wrapper
ContainerAssetFetch
allows for partial loading from sources that are considered
containers that store other assets. Typical container example would be ZIP archives,
databases, etc.
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::container::{ContainerAssetFetch, ContainerPartialFetch},
protocol::text::TextAssetProtocol,
};
use std::{error::Error, fs::File, io::Read};
use zip::ZipArchive;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
.with_protocol(TextAssetProtocol)
// Container asset fetch allows to use partial asset fetch object
// that can take asset path and returns bytes from some container.
.with_fetch(ContainerAssetFetch::new(ZipContainerPartialFetch::new(
ZipArchive::new(File::open("./resources/package.zip")?)?,
)));
let lorem = database.ensure("text://lorem.txt")?;
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
Ok(())
}
// Here we show how to make ZIP archive file reader.
struct ZipContainerPartialFetch {
archive: ZipArchive<File>,
}
impl ZipContainerPartialFetch {
pub fn new(archive: ZipArchive<File>) -> Self {
Self { archive }
}
}
impl ContainerPartialFetch for ZipContainerPartialFetch {
// We use input path and try to unpack file under that path from ZIP archive.
fn load_bytes(&mut self, path: AssetPath) -> Result<Vec<u8>, Box<dyn Error>> {
let mut file = self
.archive
.by_name(path.path())
.map_err(|error| format!("Could not read zip file: `{}` - {}", path.path(), error))?;
let mut bytes = vec![];
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
}
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::container::{ContainerAssetFetch, ContainerPartialFetch},
protocol::text::TextAssetProtocol,
};
use std::{error::Error, fs::File, io::Read};
use zip::ZipArchive;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
.with_protocol(TextAssetProtocol)
// Container asset fetch allows to use partial asset fetch object
// that can take asset path and returns bytes from some container.
.with_fetch(ContainerAssetFetch::new(ZipContainerPartialFetch::new(
ZipArchive::new(File::open("./resources/package.zip")?)?,
)));
let lorem = database.ensure("text://lorem.txt")?;
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
Ok(())
}
// Here we show how to make ZIP archive file reader.
struct ZipContainerPartialFetch {
archive: ZipArchive<File>,
}
impl ZipContainerPartialFetch {
pub fn new(archive: ZipArchive<File>) -> Self {
Self { archive }
}
}
impl ContainerPartialFetch for ZipContainerPartialFetch {
// We use input path and try to unpack file under that path from ZIP archive.
fn load_bytes(&mut self, path: AssetPath) -> Result<Vec<u8>, Box<dyn Error>> {
let mut file = self
.archive
.by_name(path.path())
.map_err(|error| format!("Could not read zip file: `{}` - {}", path.path(), error))?;
let mut bytes = vec![];
file.read_to_end(&mut bytes)?;
Ok(bytes)
}
}
Fallback assets wrapper
FallbackAssetFetch
allows to provide list of assets that should be considered
to load instead of requested assets (with matching protocol) when requested asset
fails to load - for example if requested texture does not exists, we can fallback
to popular magenta solid color texture that indicates missing textures.
use keket::{
database::AssetDatabase,
fetch::{fallback::FallbackAssetFetch, file::FileAssetFetch},
protocol::text::TextAssetProtocol,
};
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
.with_protocol(TextAssetProtocol)
.with_fetch(
// Fallback asset fetch in case of error on requested asset bytes load
// will try to load asset with matching protocol from fallback paths.
FallbackAssetFetch::new(FileAssetFetch::default().with_root("resources"))
// This fallback asset does not exists so it will be ignored.
.path("text://this-fails-to-load.txt")
// This asset exists so it will be loaded as fallback.
.path("text://lorem.txt"),
);
// This asset exists so it loads normally.
let lorem = database.ensure("text://lorem.txt")?;
// This asset does not exists so it loads fallback asset.
let non_existent = database.ensure("text://non-existent.txt")?;
if lorem.access::<&String>(&database) == non_existent.access::<&String>(&database) {
println!("Non existent asset loaded from fallback!");
}
Ok(())
}
Router fetch wrapper
RouterAssetFetch
allows to combine multiple fetch engines into routes, so that
specific fetch engine to use for given asset is decided by the pattern in asset
path - this is useful when we might want to have main game assets and additional
DLC/mod asset sources.
use keket::{
database::AssetDatabase,
fetch::{file::FileAssetFetch, router::RouterAssetFetch},
protocol::{bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
.with_fetch(
// Router allows to combine multiple asset sources, so that proper one to use
// for given asset is selected by pattern in asset path.
RouterAssetFetch::default()
// Every asset that has `router=file` meta, will load asset from file.
.route(
|path| path.has_meta_key_value("router", "file"),
FileAssetFetch::default().with_root("resources"),
0,
)
// Every asset that has `memory/` path prefix, will load from in-memory collection.
.route(
|path| path.path().starts_with("memory/"),
vec![(
"trash.bin".to_owned(),
std::fs::read("./resources/trash.bin")?,
)],
1,
),
);
// This asset will select file router.
let lorem = database.ensure("text://lorem.txt?router=file")?;
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
// This asset will select memory router.
let trash = database.ensure("bytes://memory/trash.bin")?;
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
Ok(())
}
Rewrite asset path wrapper
RewriteAssetFetch
allows to rewrite requested asset path to some other.
This is useful for scenarios like localized assets or assets versioning, where
there might be different versions of assets based on some runtime state.
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::{file::FileAssetFetch, rewrite::RewriteAssetFetch},
protocol::text::TextAssetProtocol,
};
use std::{
error::Error,
sync::{Arc, RwLock},
};
fn main() -> Result<(), Box<dyn Error>> {
let language = Arc::new(RwLock::new("en"));
let language2 = language.clone();
let mut database = AssetDatabase::default()
.with_protocol(TextAssetProtocol)
.with_fetch(RewriteAssetFetch::new(
FileAssetFetch::default().with_root("resources"),
move |path| {
// Rewrite input path to localized one.
Ok(AssetPath::from_parts(
path.protocol(),
&format!(
"{}.{}{}",
path.path_without_extension(),
*language2.read().unwrap(),
path.path_dot_extension().unwrap_or_default()
),
path.meta(),
))
},
));
// Gets `text://localized.en.txt`.
let asset = database.ensure("text://localized.txt")?;
println!("English: {}", asset.access::<&String>(&database));
// Change language.
*language.write().unwrap() = "de";
database.storage.clear();
// Gets `text://localized.de.txt`.
let asset = database.ensure("text://localized.txt")?;
println!("German: {}", asset.access::<&String>(&database));
Ok(())
}
Rewritten asset paths do not change path in asset entity - this is deliberate design decision to make outside systems not care about possible asset path change when trying to resolve asset handle by its path.
HTTP requests
Required crate:
keket-http
.
HttpAssetFetch
allows to fetch bytes with HTTP sources.
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::deferred::DeferredAssetFetch,
protocol::{bundle::BundleAssetProtocol, bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use keket_http::{third_party::reqwest::Url, HttpAssetFetch};
use serde_json::Value;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
Ok((serde_json::from_slice::<Value>(&bytes)?,).into())
}))
// HTTP asset fetch with root URL for assets.
.with_fetch(DeferredAssetFetch::new(HttpAssetFetch::new(
"https://raw.githubusercontent.com/PsichiX/Keket/refs/heads/master/resources/",
)?));
// Ensure assets exists or start getting fetched.
let lorem = database.ensure("text://lorem.txt")?;
let json = database.ensure("json://person.json")?;
let trash = database.ensure("bytes://trash.bin")?;
// Wait for pending fetches.
while database.does_await_deferred_job() {
println!("Waiting for database to be free");
println!(
"Loading:\n- Lorem Ipsum: {}\n- JSON: {}\n- Bytes: {}",
lorem.awaits_deferred_job(&database),
json.awaits_deferred_job(&database),
trash.awaits_deferred_job(&database)
);
database.maintain()?;
}
// After all assets bytes are fetched, process them into assets.
database.maintain()?;
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
println!("JSON: {:#}", json.access::<&Value>(&database));
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
// List all assets from HTTP.
for (asset_path, url) in database.storage.query::<true, (&AssetPath, &Url)>() {
println!("Asset: `{}` at url: `{}`", asset_path, url);
}
Ok(())
}
Since this is blocking fetch, you might want to wrap it with
DeferredAssetFetch
to run HTTP fetching jobs in background.
REDB database
Required crate:
keket-redb
.
RedbContainerPartialFetch
partial fetch engine allows to unpack asset bytes
from REDB database - useful for asset packs.
use keket::{
database::AssetDatabase,
fetch::container::ContainerAssetFetch,
protocol::{bundle::BundleAssetProtocol, bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use keket_redb::{third_party::redb::Database, RedbContainerPartialFetch};
use serde_json::Value;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
Ok((serde_json::from_slice::<Value>(&bytes)?,).into())
}))
.with_fetch(ContainerAssetFetch::new(RedbContainerPartialFetch::new(
Database::create("./resources/database.redb")?,
"assets",
)));
let lorem = database.ensure("text://lorem.txt")?;
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
let json = database.ensure("json://person.json")?;
println!("JSON: {:#}", json.access::<&Value>(&database));
let trash = database.ensure("bytes://trash.bin")?;
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
Ok(())
}
Since this is blocking fetch, you might want to wrap it with
DeferredAssetFetch
to run REDB fetching jobs in background.
Asset Server
Required crate:
keket-client
.
ClientAssetFetch
allows to get bytes from Keket
Asset Server (keket-server
binary crate) - a humble beginnings to DDC infrastructure for live development.
use keket::{
database::{handle::AssetHandle, path::AssetPath, AssetDatabase},
fetch::deferred::DeferredAssetFetch,
protocol::{bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use keket_client::{third_party::reqwest::Url, ClientAssetFetch};
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
// Client asset fetch to request files from asset server.
.with_fetch(DeferredAssetFetch::new(ClientAssetFetch::new(
// IP address of asset server we connect to.
"127.0.0.1:8080",
)?));
// Ensure assets exists or start getting fetched.
let lorem = database.ensure("text://lorem.txt")?;
let trash = database.ensure("bytes://trash.bin")?;
// Wait for pending fetches.
while database.does_await_deferred_job() {
println!("Waiting for database to be free");
println!(
"Loading:\n- Lorem Ipsum: {}\n- Bytes: {}",
lorem.awaits_deferred_job(&database),
trash.awaits_deferred_job(&database)
);
database.maintain()?;
}
// After all assets bytes are fetched, process them into assets.
database.maintain()?;
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
// List all assets from client.
for (asset_path, url) in database.storage.query::<true, (&AssetPath, &Url)>() {
println!("Asset: `{}` at url: `{}`", asset_path, url);
}
println!("Listening for file changes...");
loop {
database.maintain()?;
// With storage change detection we can ask for asset entities
// that their paths were updated (hot reloading updates them).
if let Some(changes) = database.storage.updated() {
for entity in changes.iter_of::<AssetPath>() {
println!(
"Asset changed: `{}`",
AssetHandle::new(entity).access::<&AssetPath>(&database)
);
}
}
// Simulate systems that detect particular asset type reload.
for entity in database.storage.added().iter_of::<String>() {
println!(
"Text asset changed: `{}`",
AssetHandle::new(entity).access::<&String>(&database)
);
}
for entity in database.storage.added().iter_of::<Vec<u8>>() {
println!(
"Bytes asset changed: `{:?}`",
AssetHandle::new(entity).access::<&Vec<u8>>(&database)
);
}
}
}
Since this is blocking fetch, you might want to wrap it with
DeferredAssetFetch
to run Asset Server fetching jobs in background.
Asset Protocol
Asset protocols are units that decode bytes and then does some things with decoded data, usually put components into asset, schedule dependencies to load and relate them with source asset, if there are any dependencies.
Asset protocols are second step in asset progression - they are decoupled from asset fetch engines to make them not care about source of the asset and by that be used no matter where assets come from, which solves the need for asset loaders to implement with all possible asset sources, which is cumbersome to deal with.
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::{bundle::BundleAssetProtocol, bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use serde::Deserialize;
use serde_json::Value;
use std::{error::Error, fs::Metadata, path::PathBuf};
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Person {
age: u8,
home: PersonHome,
friends: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct PersonHome {
country: String,
address: String,
}
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Asset protocols tell how to deserialize bytes into assets.
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
// Bundle protocol allows for easly making a protocol that takes
// bytes and returns bundle with optional dependencies.
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Value>(&bytes)?;
Ok((asset,).into())
}))
.with_protocol(BundleAssetProtocol::new("person", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Person>(&bytes)?;
Ok((asset,).into())
}))
// Asset fetch tells how to get bytes from specific source.
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure method either gives existing asset handle or creates new
// and loads asset if not existing yet in storage.
let lorem = database.ensure("text://lorem.txt")?;
// Accessing component(s) of asset entry.
// Assets can store multiple data associated to them, consider them meta data.
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
let json = database.ensure("json://person.json")?;
println!("JSON: {:#}", json.access::<&Value>(&database));
let person = database.ensure("person://person.json")?;
println!("Person: {:#?}", person.access::<&Person>(&database));
let trash = database.ensure("bytes://trash.bin")?;
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
// We can query storage for asset components to process assets, just like with ECS.
for (asset_path, file_path, metadata) in database
.storage
.query::<true, (&AssetPath, &PathBuf, &Metadata)>()
{
println!(
"Asset: `{}` at location: {:?} has metadata: {:#?}",
asset_path, file_path, metadata
);
}
Ok(())
}
For type to be considered asset protocol, it must implement AssetProtocol
trait
that has methods for processing asset bytes:
use keket::{
database::{path::AssetPathStatic, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::bundle::{
BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor,
},
};
use serde::Deserialize;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Register custom asset processor.
.with_protocol(BundleAssetProtocol::new("custom", CustomAssetProcessor))
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure custom asset existence.
let handle = database.ensure("custom://part1.json")?;
// Make database process loaded dependencies.
while database.is_busy() {
database.maintain()?;
}
let contents = handle.access::<&CustomAsset>(&database).contents(&database);
println!("Custom chain contents: {:?}", contents);
Ok(())
}
// Custom asset type.
#[derive(Debug, Default, Deserialize)]
struct CustomAsset {
// Main content.
content: String,
// Optional sibling asset (content continuation).
#[serde(default)]
next: Option<AssetPathStatic>,
}
impl CustomAsset {
fn contents(&self, database: &AssetDatabase) -> String {
// Read this and it's siblings content to output.
let mut result = String::new();
let mut current = Some(self);
while let Some(asset) = current {
result.push_str(asset.content.as_str());
current = current
.as_ref()
.and_then(|asset| asset.next.as_ref())
.and_then(|path| path.find(database))
.and_then(|handle| handle.access_checked::<&Self>(database));
if current.is_some() {
result.push(' ');
}
}
result
}
}
struct CustomAssetProcessor;
impl BundleWithDependenciesProcessor for CustomAssetProcessor {
// Bundle of asset components this asset processor produces from processed asset.
type Bundle = (CustomAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<CustomAsset>(&bytes)?;
let dependency = asset.next.clone();
// Return bundled components and optional dependency which will be additionally loaded.
Ok(BundleWithDependencies::new((asset,)).maybe_dependency(dependency))
}
}
Optionally type can implement more specific asset processing that doesn't assume you only care about bytes or even only processed asset:
use anput::world::World;
use keket::{
database::{
handle::{AssetDependency, AssetHandle},
path::AssetPathStatic,
AssetDatabase,
},
fetch::{file::FileAssetFetch, AssetAwaitsResolution, AssetBytesAreReadyToProcess},
protocol::AssetProtocol,
};
use serde::Deserialize;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Register custom asset protocol.
.with_protocol(CustomAssetProtocol)
.with_fetch(FileAssetFetch::default().with_root("resources"));
let handle = database.ensure("custom://part1.json")?;
while database.is_busy() {
database.maintain()?;
}
let contents = handle.access::<&CustomAsset>(&database).contents(&database);
println!("Custom chain contents: {:?}", contents);
Ok(())
}
// Custom asset type.
#[derive(Debug, Default, Deserialize)]
struct CustomAsset {
content: String,
#[serde(default)]
next: Option<AssetPathStatic>,
}
impl CustomAsset {
fn contents(&self, database: &AssetDatabase) -> String {
let mut result = String::new();
let mut current = Some(self);
while let Some(asset) = current {
result.push_str(asset.content.as_str());
current = current
.as_ref()
.and_then(|asset| asset.next.as_ref())
.and_then(|path| path.find(database))
.and_then(|handle| handle.access_checked::<&Self>(database));
if current.is_some() {
result.push(' ');
}
}
result
}
}
struct CustomAssetProtocol;
impl AssetProtocol for CustomAssetProtocol {
fn name(&self) -> &str {
"custom"
}
fn process_asset(
&mut self,
handle: AssetHandle,
storage: &mut World,
) -> Result<(), Box<dyn Error>> {
// Always remember that in order for asset to be considered done with processing
// bytes, it has to remove AssetBytesAreReadyToProcess component from that asset.
// We are doing that by taking its bytes content first and removing it after.
let bytes = {
let mut bytes =
storage.component_mut::<true, AssetBytesAreReadyToProcess>(handle.entity())?;
std::mem::take(&mut bytes.0)
};
storage.remove::<(AssetBytesAreReadyToProcess,)>(handle.entity())?;
// Once we have asset bytes, we decode them into asset data.
let asset = serde_json::from_slice::<CustomAsset>(&bytes)?;
// We also have to extract dependencies if it has some.
if let Some(path) = asset.next.clone() {
// For every dependency we get, we need to spawn an asset entity with
// AssetAwaitsResolution component to tell asset database to start loading it.
let entity = storage.spawn((path, AssetAwaitsResolution))?;
// We should also relate processed asset with its dependency asset.
// Dependency relations are useful for traversing asset graphs.
storage.relate::<true, _>(AssetDependency, handle.entity(), entity)?;
}
Ok(())
}
}
Bytes
BytesAssetProtocol
allows to just unwrap fetched bytes into Vec<u8>
component.
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::{bundle::BundleAssetProtocol, bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use serde::Deserialize;
use serde_json::Value;
use std::{error::Error, fs::Metadata, path::PathBuf};
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Person {
age: u8,
home: PersonHome,
friends: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct PersonHome {
country: String,
address: String,
}
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Asset protocols tell how to deserialize bytes into assets.
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
// Bundle protocol allows for easly making a protocol that takes
// bytes and returns bundle with optional dependencies.
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Value>(&bytes)?;
Ok((asset,).into())
}))
.with_protocol(BundleAssetProtocol::new("person", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Person>(&bytes)?;
Ok((asset,).into())
}))
// Asset fetch tells how to get bytes from specific source.
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure method either gives existing asset handle or creates new
// and loads asset if not existing yet in storage.
let lorem = database.ensure("text://lorem.txt")?;
// Accessing component(s) of asset entry.
// Assets can store multiple data associated to them, consider them meta data.
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
let json = database.ensure("json://person.json")?;
println!("JSON: {:#}", json.access::<&Value>(&database));
let person = database.ensure("person://person.json")?;
println!("Person: {:#?}", person.access::<&Person>(&database));
let trash = database.ensure("bytes://trash.bin")?;
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
// We can query storage for asset components to process assets, just like with ECS.
for (asset_path, file_path, metadata) in database
.storage
.query::<true, (&AssetPath, &PathBuf, &Metadata)>()
{
println!(
"Asset: `{}` at location: {:?} has metadata: {:#?}",
asset_path, file_path, metadata
);
}
Ok(())
}
Text
TextAssetProtocol
allows to decode fetched bytes into String
component.
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::{bundle::BundleAssetProtocol, bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use serde::Deserialize;
use serde_json::Value;
use std::{error::Error, fs::Metadata, path::PathBuf};
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Person {
age: u8,
home: PersonHome,
friends: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct PersonHome {
country: String,
address: String,
}
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Asset protocols tell how to deserialize bytes into assets.
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
// Bundle protocol allows for easly making a protocol that takes
// bytes and returns bundle with optional dependencies.
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Value>(&bytes)?;
Ok((asset,).into())
}))
.with_protocol(BundleAssetProtocol::new("person", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Person>(&bytes)?;
Ok((asset,).into())
}))
// Asset fetch tells how to get bytes from specific source.
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure method either gives existing asset handle or creates new
// and loads asset if not existing yet in storage.
let lorem = database.ensure("text://lorem.txt")?;
// Accessing component(s) of asset entry.
// Assets can store multiple data associated to them, consider them meta data.
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
let json = database.ensure("json://person.json")?;
println!("JSON: {:#}", json.access::<&Value>(&database));
let person = database.ensure("person://person.json")?;
println!("Person: {:#?}", person.access::<&Person>(&database));
let trash = database.ensure("bytes://trash.bin")?;
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
// We can query storage for asset components to process assets, just like with ECS.
for (asset_path, file_path, metadata) in database
.storage
.query::<true, (&AssetPath, &PathBuf, &Metadata)>()
{
println!(
"Asset: `{}` at location: {:?} has metadata: {:#?}",
asset_path, file_path, metadata
);
}
Ok(())
}
Bundle
BundleAssetProtocol
allows to create easy custom bytes decoders.
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::{bundle::BundleAssetProtocol, bytes::BytesAssetProtocol, text::TextAssetProtocol},
};
use serde::Deserialize;
use serde_json::Value;
use std::{error::Error, fs::Metadata, path::PathBuf};
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Person {
age: u8,
home: PersonHome,
friends: Vec<String>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct PersonHome {
country: String,
address: String,
}
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Asset protocols tell how to deserialize bytes into assets.
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
// Bundle protocol allows for easly making a protocol that takes
// bytes and returns bundle with optional dependencies.
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Value>(&bytes)?;
Ok((asset,).into())
}))
.with_protocol(BundleAssetProtocol::new("person", |bytes: Vec<u8>| {
let asset = serde_json::from_slice::<Person>(&bytes)?;
Ok((asset,).into())
}))
// Asset fetch tells how to get bytes from specific source.
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure method either gives existing asset handle or creates new
// and loads asset if not existing yet in storage.
let lorem = database.ensure("text://lorem.txt")?;
// Accessing component(s) of asset entry.
// Assets can store multiple data associated to them, consider them meta data.
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
let json = database.ensure("json://person.json")?;
println!("JSON: {:#}", json.access::<&Value>(&database));
let person = database.ensure("person://person.json")?;
println!("Person: {:#?}", person.access::<&Person>(&database));
let trash = database.ensure("bytes://trash.bin")?;
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
// We can query storage for asset components to process assets, just like with ECS.
for (asset_path, file_path, metadata) in database
.storage
.query::<true, (&AssetPath, &PathBuf, &Metadata)>()
{
println!(
"Asset: `{}` at location: {:?} has metadata: {:#?}",
asset_path, file_path, metadata
);
}
Ok(())
}
We can use closures or any type that implements BundleWithDependenciesProcessor
trait, which turns input bytes into BundleWithDependencies
container with output
components bundle and optional list of dependencies to schedule after decoding.
use keket::{
database::{path::AssetPathStatic, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::bundle::{
BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor,
},
};
use serde::Deserialize;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Register custom asset processor.
.with_protocol(BundleAssetProtocol::new("custom", CustomAssetProcessor))
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure custom asset existence.
let handle = database.ensure("custom://part1.json")?;
// Make database process loaded dependencies.
while database.is_busy() {
database.maintain()?;
}
let contents = handle.access::<&CustomAsset>(&database).contents(&database);
println!("Custom chain contents: {:?}", contents);
Ok(())
}
// Custom asset type.
#[derive(Debug, Default, Deserialize)]
struct CustomAsset {
// Main content.
content: String,
// Optional sibling asset (content continuation).
#[serde(default)]
next: Option<AssetPathStatic>,
}
impl CustomAsset {
fn contents(&self, database: &AssetDatabase) -> String {
// Read this and it's siblings content to output.
let mut result = String::new();
let mut current = Some(self);
while let Some(asset) = current {
result.push_str(asset.content.as_str());
current = current
.as_ref()
.and_then(|asset| asset.next.as_ref())
.and_then(|path| path.find(database))
.and_then(|handle| handle.access_checked::<&Self>(database));
if current.is_some() {
result.push(' ');
}
}
result
}
}
struct CustomAssetProcessor;
impl BundleWithDependenciesProcessor for CustomAssetProcessor {
// Bundle of asset components this asset processor produces from processed asset.
type Bundle = (CustomAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<CustomAsset>(&bytes)?;
let dependency = asset.next.clone();
// Return bundled components and optional dependency which will be additionally loaded.
Ok(BundleWithDependencies::new((asset,)).maybe_dependency(dependency))
}
}
use keket::{
database::{path::AssetPathStatic, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::bundle::{
BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor,
},
};
use serde::Deserialize;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
// Register custom asset processor.
.with_protocol(BundleAssetProtocol::new("custom", CustomAssetProcessor))
.with_fetch(FileAssetFetch::default().with_root("resources"));
// Ensure custom asset existence.
let handle = database.ensure("custom://part1.json")?;
// Make database process loaded dependencies.
while database.is_busy() {
database.maintain()?;
}
let contents = handle.access::<&CustomAsset>(&database).contents(&database);
println!("Custom chain contents: {:?}", contents);
Ok(())
}
// Custom asset type.
#[derive(Debug, Default, Deserialize)]
struct CustomAsset {
// Main content.
content: String,
// Optional sibling asset (content continuation).
#[serde(default)]
next: Option<AssetPathStatic>,
}
impl CustomAsset {
fn contents(&self, database: &AssetDatabase) -> String {
// Read this and it's siblings content to output.
let mut result = String::new();
let mut current = Some(self);
while let Some(asset) = current {
result.push_str(asset.content.as_str());
current = current
.as_ref()
.and_then(|asset| asset.next.as_ref())
.and_then(|path| path.find(database))
.and_then(|handle| handle.access_checked::<&Self>(database));
if current.is_some() {
result.push(' ');
}
}
result
}
}
struct CustomAssetProcessor;
impl BundleWithDependenciesProcessor for CustomAssetProcessor {
// Bundle of asset components this asset processor produces from processed asset.
type Bundle = (CustomAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<CustomAsset>(&bytes)?;
let dependency = asset.next.clone();
// Return bundled components and optional dependency which will be additionally loaded.
Ok(BundleWithDependencies::new((asset,)).maybe_dependency(dependency))
}
}
Group
GroupAssetProtocol
allows to load group of assets at once as its dependencies.
use keket::{
database::{path::AssetPath, AssetDatabase},
fetch::file::FileAssetFetch,
protocol::{
bundle::BundleAssetProtocol,
bytes::BytesAssetProtocol,
group::{GroupAsset, GroupAssetProtocol},
text::TextAssetProtocol,
},
};
use serde_json::Value;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let mut database = AssetDatabase::default()
.with_protocol(TextAssetProtocol)
.with_protocol(BytesAssetProtocol)
.with_protocol(BundleAssetProtocol::new("json", |bytes: Vec<u8>| {
Ok((serde_json::from_slice::<Value>(&bytes)?,).into())
}))
// Group asset protocol stores paths to other assets that gets scheduled
// for loading when group loads.
.with_protocol(GroupAssetProtocol)
.with_fetch(FileAssetFetch::default().with_root("resources"));
let group = database.ensure("group://group.txt")?;
println!("Group: {:#?}", group.access::<&GroupAsset>(&database));
while database.is_busy() {
database.maintain()?;
}
let lorem = database.ensure("text://lorem.txt")?;
println!("Lorem Ipsum: {}", lorem.access::<&String>(&database));
let json = database.ensure("json://person.json")?;
println!("JSON: {:#}", json.access::<&Value>(&database));
let trash = database.ensure("bytes://trash.bin")?;
println!("Bytes: {:?}", trash.access::<&Vec<u8>>(&database));
for dependency in group.dependencies(&database) {
println!(
"Group dependency: {}",
dependency.access::<&AssetPath>(&database)
);
}
Ok(())
}
Real life scenarios
Next sub-chapters gonna be a set of real life scenarios of problems to solve.
Basic in-game setup
Here we will showcase a basic usage of Keket
integrated with some application.
In this example we will use Spitfire crate.
See `use` section
use keket::{
database::{handle::AssetDependency, reference::AssetRef, AssetDatabase},
fetch::{deferred::DeferredAssetFetch, file::FileAssetFetch},
protocol::{
bundle::{BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor},
group::GroupAssetProtocol,
text::TextAssetProtocol,
},
third_party::anput::{
commands::{CommandBuffer, InsertCommand, RemoveCommand},
entity::Entity,
world::{Relation, World},
},
};
use serde::{Deserialize, Serialize};
use spitfire::prelude::*;
use std::{
error::Error,
sync::{Arc, RwLock},
time::Instant,
};
fn main() -> Result<(), Box<dyn Error>> {
App::<Vertex>::default().run(State::default());
Ok(())
}
const DELTA_TIME: f32 = 1.0 / 60.0;
struct State {
// We store drawing context for later use in app state.
// Drawing context holds resources and stack-based states.
context: DrawContext,
// Timer used for fixed step frame particle system simulation.
timer: Instant,
assets: AssetDatabase,
image_shader: AssetRef,
ferris_texture: AssetRef,
}
impl Default for State {
fn default() -> Self {
Self {
context: Default::default(),
timer: Instant::now(),
assets: AssetDatabase::default()
// Text protocol for shader sources.
.with_protocol(TextAssetProtocol)
// Group protocol for loading many assets at once.
.with_protocol(GroupAssetProtocol)
// Custom shader protocol.
.with_protocol(BundleAssetProtocol::new("shader", ShaderAssetProcessor))
// Custom texture protocol.
.with_protocol(BundleAssetProtocol::new("texture", TextureAssetProcessor))
// Load all data from file system asynchronously.
.with_fetch(DeferredAssetFetch::new(
FileAssetFetch::default().with_root("resources"),
)),
// Stored asset references for cached asset handles.
image_shader: AssetRef::new("shader://image.shader"),
ferris_texture: AssetRef::new("texture://ferris.png"),
}
}
}
impl AppState<Vertex> for State {
fn on_init(&mut self, graphics: &mut Graphics<Vertex>) {
// Setup scene camera.
graphics.color = [0.25, 0.25, 0.25, 1.0];
graphics.main_camera.screen_alignment = 0.5.into();
graphics.main_camera.scaling = CameraScaling::FitToView {
size: 1000.0.into(),
inside: false,
};
// Load this scene group.
self.assets.ensure("group://ingame.txt").unwrap();
}
fn on_redraw(&mut self, graphics: &mut Graphics<Vertex>) {
// Process assets periotically.
if self.timer.elapsed().as_secs_f32() > DELTA_TIME {
self.timer = Instant::now();
self.process_assets(graphics);
}
// Do not render unless we have shader loaded.
let Ok(image_shader) = self.image_shader.resolve(&self.assets) else {
return;
};
let Some(image_shader) = image_shader
.access_checked::<&AsyncHandle<Shader>>()
.map(|handle| handle.to_ref())
else {
return;
};
// Begin drawing objects.
self.context.begin_frame(graphics);
self.context.push_shader(&image_shader);
self.context.push_blending(GlowBlending::Alpha);
// Draw sprite only if texture asset is loaded.
if let Ok(texture) = self.ferris_texture.resolve(&self.assets) {
if let Some(texture) = texture
.access_checked::<&AsyncHandle<Texture>>()
.map(|handle| handle.to_ref())
{
Sprite::single(SpriteTexture::new("u_image".into(), texture))
.pivot(0.5.into())
.draw(&mut self.context, graphics);
}
}
// Commit drawn objects.
self.context.end_frame();
}
}
impl State {
fn process_assets(&mut self, graphics: &mut Graphics<Vertex>) {
let mut commands = CommandBuffer::default();
// Process loaded shader assets into shader objects on GPU.
for entity in self.assets.storage.added().iter_of::<ShaderAsset>() {
let asset = self
.assets
.storage
.component::<true, ShaderAsset>(entity)
.unwrap();
let shader = graphics.shader(&asset.vertex, &asset.fragment).unwrap();
println!("* Shader asset turned into shader: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(shader),)));
}
// Process loaded texture assets into texture objects on GPU.
for entity in self.assets.storage.added().iter_of::<TextureAsset>() {
let asset = self
.assets
.storage
.component::<true, TextureAsset>(entity)
.unwrap();
let texture = graphics
.texture(
asset.width,
asset.height,
1,
GlowTextureFormat::Rgba,
Some(&asset.bytes),
)
.unwrap();
println!("* Texture asset turned into texture: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(texture),)));
}
commands.execute(&mut self.assets.storage);
self.assets.maintain().unwrap();
}
}
// Workaround for GPU handles not being Send + Sync,
// to be able to store them in assets database.
struct AsyncHandle<T: Clone>(Arc<RwLock<T>>);
unsafe impl<T: Clone> Send for AsyncHandle<T> {}
unsafe impl<T: Clone> Sync for AsyncHandle<T> {}
impl<T: Clone> AsyncHandle<T> {
fn new(data: T) -> Self {
Self(Arc::new(RwLock::new(data)))
}
fn get(&self) -> T {
self.0.read().unwrap().clone()
}
fn to_ref(&self) -> ResourceRef<T> {
ResourceRef::object(self.get())
}
}
// Decoded shader asset information with dependencies.
#[derive(Debug, Serialize, Deserialize)]
struct ShaderAssetInfo {
vertex: AssetRef,
fragment: AssetRef,
}
// Shader asset with vertex and fragment programs code.
struct ShaderAsset {
vertex: String,
fragment: String,
}
// Shader asset processor that turns bytes -> shader info -> shader asset.
struct ShaderAssetProcessor;
impl BundleWithDependenciesProcessor for ShaderAssetProcessor {
type Bundle = (ShaderAssetInfo,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<ShaderAssetInfo>(&bytes)?;
let vertex = asset.vertex.path().clone();
let fragment = asset.fragment.path().clone();
println!("* Shader asset processed: {:#?}", asset);
Ok(BundleWithDependencies::new((asset,))
.dependency(vertex)
.dependency(fragment))
}
fn maintain(&mut self, storage: &mut World) -> Result<(), Box<dyn Error>> {
let mut commands = CommandBuffer::default();
let mut lookup = storage.lookup_access::<true, &String>();
// We scan for decoded shader info and if dependencies are loaded,
// then turn them into shader asset.
for (entity, info, dependencies) in
storage.query::<true, (Entity, &ShaderAssetInfo, &Relation<AssetDependency>)>()
{
if dependencies
.entities()
.all(|entity| storage.has_entity_component::<String>(entity))
{
let vertex = lookup
.access(storage.find_by::<true, _>(info.vertex.path()).unwrap())
.unwrap()
.to_owned();
let fragment = lookup
.access(storage.find_by::<true, _>(info.fragment.path()).unwrap())
.unwrap()
.to_owned();
let asset = ShaderAsset { vertex, fragment };
commands.command(InsertCommand::new(entity, (asset,)));
commands.command(RemoveCommand::<(ShaderAssetInfo,)>::new(entity));
}
}
drop(lookup);
commands.execute(storage);
Ok(())
}
}
// Decoded texture asset with its size and decoded bitmap bytes.
struct TextureAsset {
width: u32,
height: u32,
bytes: Vec<u8>,
}
struct TextureAssetProcessor;
impl BundleWithDependenciesProcessor for TextureAssetProcessor {
type Bundle = (TextureAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
// Decode PNG image into texture size and bitmap bytes.
let decoder = png::Decoder::new(bytes.as_slice());
let mut reader = decoder.read_info()?;
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf)?;
let bytes = buf[..info.buffer_size()].to_vec();
println!("* Texture asset processed: {:#?}", info);
Ok(BundleWithDependencies::new((TextureAsset {
width: info.width,
height: info.height,
bytes,
},)))
}
}
Main function
Main function looks boring - all we do is we run App
with State
object.
use keket::{
database::{handle::AssetDependency, reference::AssetRef, AssetDatabase},
fetch::{deferred::DeferredAssetFetch, file::FileAssetFetch},
protocol::{
bundle::{BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor},
group::GroupAssetProtocol,
text::TextAssetProtocol,
},
third_party::anput::{
commands::{CommandBuffer, InsertCommand, RemoveCommand},
entity::Entity,
world::{Relation, World},
},
};
use serde::{Deserialize, Serialize};
use spitfire::prelude::*;
use std::{
error::Error,
sync::{Arc, RwLock},
time::Instant,
};
fn main() -> Result<(), Box<dyn Error>> {
App::<Vertex>::default().run(State::default());
Ok(())
}
const DELTA_TIME: f32 = 1.0 / 60.0;
struct State {
// We store drawing context for later use in app state.
// Drawing context holds resources and stack-based states.
context: DrawContext,
// Timer used for fixed step frame particle system simulation.
timer: Instant,
assets: AssetDatabase,
image_shader: AssetRef,
ferris_texture: AssetRef,
}
impl Default for State {
fn default() -> Self {
Self {
context: Default::default(),
timer: Instant::now(),
assets: AssetDatabase::default()
// Text protocol for shader sources.
.with_protocol(TextAssetProtocol)
// Group protocol for loading many assets at once.
.with_protocol(GroupAssetProtocol)
// Custom shader protocol.
.with_protocol(BundleAssetProtocol::new("shader", ShaderAssetProcessor))
// Custom texture protocol.
.with_protocol(BundleAssetProtocol::new("texture", TextureAssetProcessor))
// Load all data from file system asynchronously.
.with_fetch(DeferredAssetFetch::new(
FileAssetFetch::default().with_root("resources"),
)),
// Stored asset references for cached asset handles.
image_shader: AssetRef::new("shader://image.shader"),
ferris_texture: AssetRef::new("texture://ferris.png"),
}
}
}
impl AppState<Vertex> for State {
fn on_init(&mut self, graphics: &mut Graphics<Vertex>) {
// Setup scene camera.
graphics.color = [0.25, 0.25, 0.25, 1.0];
graphics.main_camera.screen_alignment = 0.5.into();
graphics.main_camera.scaling = CameraScaling::FitToView {
size: 1000.0.into(),
inside: false,
};
// Load this scene group.
self.assets.ensure("group://ingame.txt").unwrap();
}
fn on_redraw(&mut self, graphics: &mut Graphics<Vertex>) {
// Process assets periotically.
if self.timer.elapsed().as_secs_f32() > DELTA_TIME {
self.timer = Instant::now();
self.process_assets(graphics);
}
// Do not render unless we have shader loaded.
let Ok(image_shader) = self.image_shader.resolve(&self.assets) else {
return;
};
let Some(image_shader) = image_shader
.access_checked::<&AsyncHandle<Shader>>()
.map(|handle| handle.to_ref())
else {
return;
};
// Begin drawing objects.
self.context.begin_frame(graphics);
self.context.push_shader(&image_shader);
self.context.push_blending(GlowBlending::Alpha);
// Draw sprite only if texture asset is loaded.
if let Ok(texture) = self.ferris_texture.resolve(&self.assets) {
if let Some(texture) = texture
.access_checked::<&AsyncHandle<Texture>>()
.map(|handle| handle.to_ref())
{
Sprite::single(SpriteTexture::new("u_image".into(), texture))
.pivot(0.5.into())
.draw(&mut self.context, graphics);
}
}
// Commit drawn objects.
self.context.end_frame();
}
}
impl State {
fn process_assets(&mut self, graphics: &mut Graphics<Vertex>) {
let mut commands = CommandBuffer::default();
// Process loaded shader assets into shader objects on GPU.
for entity in self.assets.storage.added().iter_of::<ShaderAsset>() {
let asset = self
.assets
.storage
.component::<true, ShaderAsset>(entity)
.unwrap();
let shader = graphics.shader(&asset.vertex, &asset.fragment).unwrap();
println!("* Shader asset turned into shader: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(shader),)));
}
// Process loaded texture assets into texture objects on GPU.
for entity in self.assets.storage.added().iter_of::<TextureAsset>() {
let asset = self
.assets
.storage
.component::<true, TextureAsset>(entity)
.unwrap();
let texture = graphics
.texture(
asset.width,
asset.height,
1,
GlowTextureFormat::Rgba,
Some(&asset.bytes),
)
.unwrap();
println!("* Texture asset turned into texture: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(texture),)));
}
commands.execute(&mut self.assets.storage);
self.assets.maintain().unwrap();
}
}
// Workaround for GPU handles not being Send + Sync,
// to be able to store them in assets database.
struct AsyncHandle<T: Clone>(Arc<RwLock<T>>);
unsafe impl<T: Clone> Send for AsyncHandle<T> {}
unsafe impl<T: Clone> Sync for AsyncHandle<T> {}
impl<T: Clone> AsyncHandle<T> {
fn new(data: T) -> Self {
Self(Arc::new(RwLock::new(data)))
}
fn get(&self) -> T {
self.0.read().unwrap().clone()
}
fn to_ref(&self) -> ResourceRef<T> {
ResourceRef::object(self.get())
}
}
// Decoded shader asset information with dependencies.
#[derive(Debug, Serialize, Deserialize)]
struct ShaderAssetInfo {
vertex: AssetRef,
fragment: AssetRef,
}
// Shader asset with vertex and fragment programs code.
struct ShaderAsset {
vertex: String,
fragment: String,
}
// Shader asset processor that turns bytes -> shader info -> shader asset.
struct ShaderAssetProcessor;
impl BundleWithDependenciesProcessor for ShaderAssetProcessor {
type Bundle = (ShaderAssetInfo,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<ShaderAssetInfo>(&bytes)?;
let vertex = asset.vertex.path().clone();
let fragment = asset.fragment.path().clone();
println!("* Shader asset processed: {:#?}", asset);
Ok(BundleWithDependencies::new((asset,))
.dependency(vertex)
.dependency(fragment))
}
fn maintain(&mut self, storage: &mut World) -> Result<(), Box<dyn Error>> {
let mut commands = CommandBuffer::default();
let mut lookup = storage.lookup_access::<true, &String>();
// We scan for decoded shader info and if dependencies are loaded,
// then turn them into shader asset.
for (entity, info, dependencies) in
storage.query::<true, (Entity, &ShaderAssetInfo, &Relation<AssetDependency>)>()
{
if dependencies
.entities()
.all(|entity| storage.has_entity_component::<String>(entity))
{
let vertex = lookup
.access(storage.find_by::<true, _>(info.vertex.path()).unwrap())
.unwrap()
.to_owned();
let fragment = lookup
.access(storage.find_by::<true, _>(info.fragment.path()).unwrap())
.unwrap()
.to_owned();
let asset = ShaderAsset { vertex, fragment };
commands.command(InsertCommand::new(entity, (asset,)));
commands.command(RemoveCommand::<(ShaderAssetInfo,)>::new(entity));
}
}
drop(lookup);
commands.execute(storage);
Ok(())
}
}
// Decoded texture asset with its size and decoded bitmap bytes.
struct TextureAsset {
width: u32,
height: u32,
bytes: Vec<u8>,
}
struct TextureAssetProcessor;
impl BundleWithDependenciesProcessor for TextureAssetProcessor {
type Bundle = (TextureAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
// Decode PNG image into texture size and bitmap bytes.
let decoder = png::Decoder::new(bytes.as_slice());
let mut reader = decoder.read_info()?;
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf)?;
let bytes = buf[..info.buffer_size()].to_vec();
println!("* Texture asset processed: {:#?}", info);
Ok(BundleWithDependencies::new((TextureAsset {
width: info.width,
height: info.height,
bytes,
},)))
}
}
State struct
State
type holds AssetDatabase
along with some other data useful for drawing,
fixed time step mechanism and referencing assets.
use keket::{
database::{handle::AssetDependency, reference::AssetRef, AssetDatabase},
fetch::{deferred::DeferredAssetFetch, file::FileAssetFetch},
protocol::{
bundle::{BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor},
group::GroupAssetProtocol,
text::TextAssetProtocol,
},
third_party::anput::{
commands::{CommandBuffer, InsertCommand, RemoveCommand},
entity::Entity,
world::{Relation, World},
},
};
use serde::{Deserialize, Serialize};
use spitfire::prelude::*;
use std::{
error::Error,
sync::{Arc, RwLock},
time::Instant,
};
fn main() -> Result<(), Box<dyn Error>> {
App::<Vertex>::default().run(State::default());
Ok(())
}
const DELTA_TIME: f32 = 1.0 / 60.0;
struct State {
// We store drawing context for later use in app state.
// Drawing context holds resources and stack-based states.
context: DrawContext,
// Timer used for fixed step frame particle system simulation.
timer: Instant,
assets: AssetDatabase,
image_shader: AssetRef,
ferris_texture: AssetRef,
}
impl Default for State {
fn default() -> Self {
Self {
context: Default::default(),
timer: Instant::now(),
assets: AssetDatabase::default()
// Text protocol for shader sources.
.with_protocol(TextAssetProtocol)
// Group protocol for loading many assets at once.
.with_protocol(GroupAssetProtocol)
// Custom shader protocol.
.with_protocol(BundleAssetProtocol::new("shader", ShaderAssetProcessor))
// Custom texture protocol.
.with_protocol(BundleAssetProtocol::new("texture", TextureAssetProcessor))
// Load all data from file system asynchronously.
.with_fetch(DeferredAssetFetch::new(
FileAssetFetch::default().with_root("resources"),
)),
// Stored asset references for cached asset handles.
image_shader: AssetRef::new("shader://image.shader"),
ferris_texture: AssetRef::new("texture://ferris.png"),
}
}
}
impl AppState<Vertex> for State {
fn on_init(&mut self, graphics: &mut Graphics<Vertex>) {
// Setup scene camera.
graphics.color = [0.25, 0.25, 0.25, 1.0];
graphics.main_camera.screen_alignment = 0.5.into();
graphics.main_camera.scaling = CameraScaling::FitToView {
size: 1000.0.into(),
inside: false,
};
// Load this scene group.
self.assets.ensure("group://ingame.txt").unwrap();
}
fn on_redraw(&mut self, graphics: &mut Graphics<Vertex>) {
// Process assets periotically.
if self.timer.elapsed().as_secs_f32() > DELTA_TIME {
self.timer = Instant::now();
self.process_assets(graphics);
}
// Do not render unless we have shader loaded.
let Ok(image_shader) = self.image_shader.resolve(&self.assets) else {
return;
};
let Some(image_shader) = image_shader
.access_checked::<&AsyncHandle<Shader>>()
.map(|handle| handle.to_ref())
else {
return;
};
// Begin drawing objects.
self.context.begin_frame(graphics);
self.context.push_shader(&image_shader);
self.context.push_blending(GlowBlending::Alpha);
// Draw sprite only if texture asset is loaded.
if let Ok(texture) = self.ferris_texture.resolve(&self.assets) {
if let Some(texture) = texture
.access_checked::<&AsyncHandle<Texture>>()
.map(|handle| handle.to_ref())
{
Sprite::single(SpriteTexture::new("u_image".into(), texture))
.pivot(0.5.into())
.draw(&mut self.context, graphics);
}
}
// Commit drawn objects.
self.context.end_frame();
}
}
impl State {
fn process_assets(&mut self, graphics: &mut Graphics<Vertex>) {
let mut commands = CommandBuffer::default();
// Process loaded shader assets into shader objects on GPU.
for entity in self.assets.storage.added().iter_of::<ShaderAsset>() {
let asset = self
.assets
.storage
.component::<true, ShaderAsset>(entity)
.unwrap();
let shader = graphics.shader(&asset.vertex, &asset.fragment).unwrap();
println!("* Shader asset turned into shader: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(shader),)));
}
// Process loaded texture assets into texture objects on GPU.
for entity in self.assets.storage.added().iter_of::<TextureAsset>() {
let asset = self
.assets
.storage
.component::<true, TextureAsset>(entity)
.unwrap();
let texture = graphics
.texture(
asset.width,
asset.height,
1,
GlowTextureFormat::Rgba,
Some(&asset.bytes),
)
.unwrap();
println!("* Texture asset turned into texture: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(texture),)));
}
commands.execute(&mut self.assets.storage);
self.assets.maintain().unwrap();
}
}
// Workaround for GPU handles not being Send + Sync,
// to be able to store them in assets database.
struct AsyncHandle<T: Clone>(Arc<RwLock<T>>);
unsafe impl<T: Clone> Send for AsyncHandle<T> {}
unsafe impl<T: Clone> Sync for AsyncHandle<T> {}
impl<T: Clone> AsyncHandle<T> {
fn new(data: T) -> Self {
Self(Arc::new(RwLock::new(data)))
}
fn get(&self) -> T {
self.0.read().unwrap().clone()
}
fn to_ref(&self) -> ResourceRef<T> {
ResourceRef::object(self.get())
}
}
// Decoded shader asset information with dependencies.
#[derive(Debug, Serialize, Deserialize)]
struct ShaderAssetInfo {
vertex: AssetRef,
fragment: AssetRef,
}
// Shader asset with vertex and fragment programs code.
struct ShaderAsset {
vertex: String,
fragment: String,
}
// Shader asset processor that turns bytes -> shader info -> shader asset.
struct ShaderAssetProcessor;
impl BundleWithDependenciesProcessor for ShaderAssetProcessor {
type Bundle = (ShaderAssetInfo,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<ShaderAssetInfo>(&bytes)?;
let vertex = asset.vertex.path().clone();
let fragment = asset.fragment.path().clone();
println!("* Shader asset processed: {:#?}", asset);
Ok(BundleWithDependencies::new((asset,))
.dependency(vertex)
.dependency(fragment))
}
fn maintain(&mut self, storage: &mut World) -> Result<(), Box<dyn Error>> {
let mut commands = CommandBuffer::default();
let mut lookup = storage.lookup_access::<true, &String>();
// We scan for decoded shader info and if dependencies are loaded,
// then turn them into shader asset.
for (entity, info, dependencies) in
storage.query::<true, (Entity, &ShaderAssetInfo, &Relation<AssetDependency>)>()
{
if dependencies
.entities()
.all(|entity| storage.has_entity_component::<String>(entity))
{
let vertex = lookup
.access(storage.find_by::<true, _>(info.vertex.path()).unwrap())
.unwrap()
.to_owned();
let fragment = lookup
.access(storage.find_by::<true, _>(info.fragment.path()).unwrap())
.unwrap()
.to_owned();
let asset = ShaderAsset { vertex, fragment };
commands.command(InsertCommand::new(entity, (asset,)));
commands.command(RemoveCommand::<(ShaderAssetInfo,)>::new(entity));
}
}
drop(lookup);
commands.execute(storage);
Ok(())
}
}
// Decoded texture asset with its size and decoded bitmap bytes.
struct TextureAsset {
width: u32,
height: u32,
bytes: Vec<u8>,
}
struct TextureAssetProcessor;
impl BundleWithDependenciesProcessor for TextureAssetProcessor {
type Bundle = (TextureAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
// Decode PNG image into texture size and bitmap bytes.
let decoder = png::Decoder::new(bytes.as_slice());
let mut reader = decoder.read_info()?;
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf)?;
let bytes = buf[..info.buffer_size()].to_vec();
println!("* Texture asset processed: {:#?}", info);
Ok(BundleWithDependencies::new((TextureAsset {
width: info.width,
height: info.height,
bytes,
},)))
}
}
State Default
impl
In Default
implementatio we setup app state.
Take a look at how we setup AssetDatabase
protocols:
use keket::{
database::{handle::AssetDependency, reference::AssetRef, AssetDatabase},
fetch::{deferred::DeferredAssetFetch, file::FileAssetFetch},
protocol::{
bundle::{BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor},
group::GroupAssetProtocol,
text::TextAssetProtocol,
},
third_party::anput::{
commands::{CommandBuffer, InsertCommand, RemoveCommand},
entity::Entity,
world::{Relation, World},
},
};
use serde::{Deserialize, Serialize};
use spitfire::prelude::*;
use std::{
error::Error,
sync::{Arc, RwLock},
time::Instant,
};
fn main() -> Result<(), Box<dyn Error>> {
App::<Vertex>::default().run(State::default());
Ok(())
}
const DELTA_TIME: f32 = 1.0 / 60.0;
struct State {
// We store drawing context for later use in app state.
// Drawing context holds resources and stack-based states.
context: DrawContext,
// Timer used for fixed step frame particle system simulation.
timer: Instant,
assets: AssetDatabase,
image_shader: AssetRef,
ferris_texture: AssetRef,
}
impl Default for State {
fn default() -> Self {
Self {
context: Default::default(),
timer: Instant::now(),
assets: AssetDatabase::default()
// Text protocol for shader sources.
.with_protocol(TextAssetProtocol)
// Group protocol for loading many assets at once.
.with_protocol(GroupAssetProtocol)
// Custom shader protocol.
.with_protocol(BundleAssetProtocol::new("shader", ShaderAssetProcessor))
// Custom texture protocol.
.with_protocol(BundleAssetProtocol::new("texture", TextureAssetProcessor))
// Load all data from file system asynchronously.
.with_fetch(DeferredAssetFetch::new(
FileAssetFetch::default().with_root("resources"),
)),
// Stored asset references for cached asset handles.
image_shader: AssetRef::new("shader://image.shader"),
ferris_texture: AssetRef::new("texture://ferris.png"),
}
}
}
impl AppState<Vertex> for State {
fn on_init(&mut self, graphics: &mut Graphics<Vertex>) {
// Setup scene camera.
graphics.color = [0.25, 0.25, 0.25, 1.0];
graphics.main_camera.screen_alignment = 0.5.into();
graphics.main_camera.scaling = CameraScaling::FitToView {
size: 1000.0.into(),
inside: false,
};
// Load this scene group.
self.assets.ensure("group://ingame.txt").unwrap();
}
fn on_redraw(&mut self, graphics: &mut Graphics<Vertex>) {
// Process assets periotically.
if self.timer.elapsed().as_secs_f32() > DELTA_TIME {
self.timer = Instant::now();
self.process_assets(graphics);
}
// Do not render unless we have shader loaded.
let Ok(image_shader) = self.image_shader.resolve(&self.assets) else {
return;
};
let Some(image_shader) = image_shader
.access_checked::<&AsyncHandle<Shader>>()
.map(|handle| handle.to_ref())
else {
return;
};
// Begin drawing objects.
self.context.begin_frame(graphics);
self.context.push_shader(&image_shader);
self.context.push_blending(GlowBlending::Alpha);
// Draw sprite only if texture asset is loaded.
if let Ok(texture) = self.ferris_texture.resolve(&self.assets) {
if let Some(texture) = texture
.access_checked::<&AsyncHandle<Texture>>()
.map(|handle| handle.to_ref())
{
Sprite::single(SpriteTexture::new("u_image".into(), texture))
.pivot(0.5.into())
.draw(&mut self.context, graphics);
}
}
// Commit drawn objects.
self.context.end_frame();
}
}
impl State {
fn process_assets(&mut self, graphics: &mut Graphics<Vertex>) {
let mut commands = CommandBuffer::default();
// Process loaded shader assets into shader objects on GPU.
for entity in self.assets.storage.added().iter_of::<ShaderAsset>() {
let asset = self
.assets
.storage
.component::<true, ShaderAsset>(entity)
.unwrap();
let shader = graphics.shader(&asset.vertex, &asset.fragment).unwrap();
println!("* Shader asset turned into shader: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(shader),)));
}
// Process loaded texture assets into texture objects on GPU.
for entity in self.assets.storage.added().iter_of::<TextureAsset>() {
let asset = self
.assets
.storage
.component::<true, TextureAsset>(entity)
.unwrap();
let texture = graphics
.texture(
asset.width,
asset.height,
1,
GlowTextureFormat::Rgba,
Some(&asset.bytes),
)
.unwrap();
println!("* Texture asset turned into texture: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(texture),)));
}
commands.execute(&mut self.assets.storage);
self.assets.maintain().unwrap();
}
}
// Workaround for GPU handles not being Send + Sync,
// to be able to store them in assets database.
struct AsyncHandle<T: Clone>(Arc<RwLock<T>>);
unsafe impl<T: Clone> Send for AsyncHandle<T> {}
unsafe impl<T: Clone> Sync for AsyncHandle<T> {}
impl<T: Clone> AsyncHandle<T> {
fn new(data: T) -> Self {
Self(Arc::new(RwLock::new(data)))
}
fn get(&self) -> T {
self.0.read().unwrap().clone()
}
fn to_ref(&self) -> ResourceRef<T> {
ResourceRef::object(self.get())
}
}
// Decoded shader asset information with dependencies.
#[derive(Debug, Serialize, Deserialize)]
struct ShaderAssetInfo {
vertex: AssetRef,
fragment: AssetRef,
}
// Shader asset with vertex and fragment programs code.
struct ShaderAsset {
vertex: String,
fragment: String,
}
// Shader asset processor that turns bytes -> shader info -> shader asset.
struct ShaderAssetProcessor;
impl BundleWithDependenciesProcessor for ShaderAssetProcessor {
type Bundle = (ShaderAssetInfo,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<ShaderAssetInfo>(&bytes)?;
let vertex = asset.vertex.path().clone();
let fragment = asset.fragment.path().clone();
println!("* Shader asset processed: {:#?}", asset);
Ok(BundleWithDependencies::new((asset,))
.dependency(vertex)
.dependency(fragment))
}
fn maintain(&mut self, storage: &mut World) -> Result<(), Box<dyn Error>> {
let mut commands = CommandBuffer::default();
let mut lookup = storage.lookup_access::<true, &String>();
// We scan for decoded shader info and if dependencies are loaded,
// then turn them into shader asset.
for (entity, info, dependencies) in
storage.query::<true, (Entity, &ShaderAssetInfo, &Relation<AssetDependency>)>()
{
if dependencies
.entities()
.all(|entity| storage.has_entity_component::<String>(entity))
{
let vertex = lookup
.access(storage.find_by::<true, _>(info.vertex.path()).unwrap())
.unwrap()
.to_owned();
let fragment = lookup
.access(storage.find_by::<true, _>(info.fragment.path()).unwrap())
.unwrap()
.to_owned();
let asset = ShaderAsset { vertex, fragment };
commands.command(InsertCommand::new(entity, (asset,)));
commands.command(RemoveCommand::<(ShaderAssetInfo,)>::new(entity));
}
}
drop(lookup);
commands.execute(storage);
Ok(())
}
}
// Decoded texture asset with its size and decoded bitmap bytes.
struct TextureAsset {
width: u32,
height: u32,
bytes: Vec<u8>,
}
struct TextureAssetProcessor;
impl BundleWithDependenciesProcessor for TextureAssetProcessor {
type Bundle = (TextureAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
// Decode PNG image into texture size and bitmap bytes.
let decoder = png::Decoder::new(bytes.as_slice());
let mut reader = decoder.read_info()?;
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf)?;
let bytes = buf[..info.buffer_size()].to_vec();
println!("* Texture asset processed: {:#?}", info);
Ok(BundleWithDependencies::new((TextureAsset {
width: info.width,
height: info.height,
bytes,
},)))
}
}
In there you can see bundle asset protocols wrapping custom shader and texture asset processors.
State AppState
impl
Then we implement AppState
for our State
type, where in on_init
we setup
graphics and load scene group asset, and in on_redraw
we run asset database
maintanance periodically and then try to render Ferris sprite only if shader and
texture assets are ready.
use keket::{
database::{handle::AssetDependency, reference::AssetRef, AssetDatabase},
fetch::{deferred::DeferredAssetFetch, file::FileAssetFetch},
protocol::{
bundle::{BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor},
group::GroupAssetProtocol,
text::TextAssetProtocol,
},
third_party::anput::{
commands::{CommandBuffer, InsertCommand, RemoveCommand},
entity::Entity,
world::{Relation, World},
},
};
use serde::{Deserialize, Serialize};
use spitfire::prelude::*;
use std::{
error::Error,
sync::{Arc, RwLock},
time::Instant,
};
fn main() -> Result<(), Box<dyn Error>> {
App::<Vertex>::default().run(State::default());
Ok(())
}
const DELTA_TIME: f32 = 1.0 / 60.0;
struct State {
// We store drawing context for later use in app state.
// Drawing context holds resources and stack-based states.
context: DrawContext,
// Timer used for fixed step frame particle system simulation.
timer: Instant,
assets: AssetDatabase,
image_shader: AssetRef,
ferris_texture: AssetRef,
}
impl Default for State {
fn default() -> Self {
Self {
context: Default::default(),
timer: Instant::now(),
assets: AssetDatabase::default()
// Text protocol for shader sources.
.with_protocol(TextAssetProtocol)
// Group protocol for loading many assets at once.
.with_protocol(GroupAssetProtocol)
// Custom shader protocol.
.with_protocol(BundleAssetProtocol::new("shader", ShaderAssetProcessor))
// Custom texture protocol.
.with_protocol(BundleAssetProtocol::new("texture", TextureAssetProcessor))
// Load all data from file system asynchronously.
.with_fetch(DeferredAssetFetch::new(
FileAssetFetch::default().with_root("resources"),
)),
// Stored asset references for cached asset handles.
image_shader: AssetRef::new("shader://image.shader"),
ferris_texture: AssetRef::new("texture://ferris.png"),
}
}
}
impl AppState<Vertex> for State {
fn on_init(&mut self, graphics: &mut Graphics<Vertex>) {
// Setup scene camera.
graphics.color = [0.25, 0.25, 0.25, 1.0];
graphics.main_camera.screen_alignment = 0.5.into();
graphics.main_camera.scaling = CameraScaling::FitToView {
size: 1000.0.into(),
inside: false,
};
// Load this scene group.
self.assets.ensure("group://ingame.txt").unwrap();
}
fn on_redraw(&mut self, graphics: &mut Graphics<Vertex>) {
// Process assets periotically.
if self.timer.elapsed().as_secs_f32() > DELTA_TIME {
self.timer = Instant::now();
self.process_assets(graphics);
}
// Do not render unless we have shader loaded.
let Ok(image_shader) = self.image_shader.resolve(&self.assets) else {
return;
};
let Some(image_shader) = image_shader
.access_checked::<&AsyncHandle<Shader>>()
.map(|handle| handle.to_ref())
else {
return;
};
// Begin drawing objects.
self.context.begin_frame(graphics);
self.context.push_shader(&image_shader);
self.context.push_blending(GlowBlending::Alpha);
// Draw sprite only if texture asset is loaded.
if let Ok(texture) = self.ferris_texture.resolve(&self.assets) {
if let Some(texture) = texture
.access_checked::<&AsyncHandle<Texture>>()
.map(|handle| handle.to_ref())
{
Sprite::single(SpriteTexture::new("u_image".into(), texture))
.pivot(0.5.into())
.draw(&mut self.context, graphics);
}
}
// Commit drawn objects.
self.context.end_frame();
}
}
impl State {
fn process_assets(&mut self, graphics: &mut Graphics<Vertex>) {
let mut commands = CommandBuffer::default();
// Process loaded shader assets into shader objects on GPU.
for entity in self.assets.storage.added().iter_of::<ShaderAsset>() {
let asset = self
.assets
.storage
.component::<true, ShaderAsset>(entity)
.unwrap();
let shader = graphics.shader(&asset.vertex, &asset.fragment).unwrap();
println!("* Shader asset turned into shader: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(shader),)));
}
// Process loaded texture assets into texture objects on GPU.
for entity in self.assets.storage.added().iter_of::<TextureAsset>() {
let asset = self
.assets
.storage
.component::<true, TextureAsset>(entity)
.unwrap();
let texture = graphics
.texture(
asset.width,
asset.height,
1,
GlowTextureFormat::Rgba,
Some(&asset.bytes),
)
.unwrap();
println!("* Texture asset turned into texture: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(texture),)));
}
commands.execute(&mut self.assets.storage);
self.assets.maintain().unwrap();
}
}
// Workaround for GPU handles not being Send + Sync,
// to be able to store them in assets database.
struct AsyncHandle<T: Clone>(Arc<RwLock<T>>);
unsafe impl<T: Clone> Send for AsyncHandle<T> {}
unsafe impl<T: Clone> Sync for AsyncHandle<T> {}
impl<T: Clone> AsyncHandle<T> {
fn new(data: T) -> Self {
Self(Arc::new(RwLock::new(data)))
}
fn get(&self) -> T {
self.0.read().unwrap().clone()
}
fn to_ref(&self) -> ResourceRef<T> {
ResourceRef::object(self.get())
}
}
// Decoded shader asset information with dependencies.
#[derive(Debug, Serialize, Deserialize)]
struct ShaderAssetInfo {
vertex: AssetRef,
fragment: AssetRef,
}
// Shader asset with vertex and fragment programs code.
struct ShaderAsset {
vertex: String,
fragment: String,
}
// Shader asset processor that turns bytes -> shader info -> shader asset.
struct ShaderAssetProcessor;
impl BundleWithDependenciesProcessor for ShaderAssetProcessor {
type Bundle = (ShaderAssetInfo,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<ShaderAssetInfo>(&bytes)?;
let vertex = asset.vertex.path().clone();
let fragment = asset.fragment.path().clone();
println!("* Shader asset processed: {:#?}", asset);
Ok(BundleWithDependencies::new((asset,))
.dependency(vertex)
.dependency(fragment))
}
fn maintain(&mut self, storage: &mut World) -> Result<(), Box<dyn Error>> {
let mut commands = CommandBuffer::default();
let mut lookup = storage.lookup_access::<true, &String>();
// We scan for decoded shader info and if dependencies are loaded,
// then turn them into shader asset.
for (entity, info, dependencies) in
storage.query::<true, (Entity, &ShaderAssetInfo, &Relation<AssetDependency>)>()
{
if dependencies
.entities()
.all(|entity| storage.has_entity_component::<String>(entity))
{
let vertex = lookup
.access(storage.find_by::<true, _>(info.vertex.path()).unwrap())
.unwrap()
.to_owned();
let fragment = lookup
.access(storage.find_by::<true, _>(info.fragment.path()).unwrap())
.unwrap()
.to_owned();
let asset = ShaderAsset { vertex, fragment };
commands.command(InsertCommand::new(entity, (asset,)));
commands.command(RemoveCommand::<(ShaderAssetInfo,)>::new(entity));
}
}
drop(lookup);
commands.execute(storage);
Ok(())
}
}
// Decoded texture asset with its size and decoded bitmap bytes.
struct TextureAsset {
width: u32,
height: u32,
bytes: Vec<u8>,
}
struct TextureAssetProcessor;
impl BundleWithDependenciesProcessor for TextureAssetProcessor {
type Bundle = (TextureAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
// Decode PNG image into texture size and bitmap bytes.
let decoder = png::Decoder::new(bytes.as_slice());
let mut reader = decoder.read_info()?;
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf)?;
let bytes = buf[..info.buffer_size()].to_vec();
println!("* Texture asset processed: {:#?}", info);
Ok(BundleWithDependencies::new((TextureAsset {
width: info.width,
height: info.height,
bytes,
},)))
}
}
State impl
In State
implementation we finally do our first interesting bit - since asset
protocols does not have access to our state, we can process prepared shader and
texture assets and then build GPU objects for these assets and put them into
their assets.
use keket::{
database::{handle::AssetDependency, reference::AssetRef, AssetDatabase},
fetch::{deferred::DeferredAssetFetch, file::FileAssetFetch},
protocol::{
bundle::{BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor},
group::GroupAssetProtocol,
text::TextAssetProtocol,
},
third_party::anput::{
commands::{CommandBuffer, InsertCommand, RemoveCommand},
entity::Entity,
world::{Relation, World},
},
};
use serde::{Deserialize, Serialize};
use spitfire::prelude::*;
use std::{
error::Error,
sync::{Arc, RwLock},
time::Instant,
};
fn main() -> Result<(), Box<dyn Error>> {
App::<Vertex>::default().run(State::default());
Ok(())
}
const DELTA_TIME: f32 = 1.0 / 60.0;
struct State {
// We store drawing context for later use in app state.
// Drawing context holds resources and stack-based states.
context: DrawContext,
// Timer used for fixed step frame particle system simulation.
timer: Instant,
assets: AssetDatabase,
image_shader: AssetRef,
ferris_texture: AssetRef,
}
impl Default for State {
fn default() -> Self {
Self {
context: Default::default(),
timer: Instant::now(),
assets: AssetDatabase::default()
// Text protocol for shader sources.
.with_protocol(TextAssetProtocol)
// Group protocol for loading many assets at once.
.with_protocol(GroupAssetProtocol)
// Custom shader protocol.
.with_protocol(BundleAssetProtocol::new("shader", ShaderAssetProcessor))
// Custom texture protocol.
.with_protocol(BundleAssetProtocol::new("texture", TextureAssetProcessor))
// Load all data from file system asynchronously.
.with_fetch(DeferredAssetFetch::new(
FileAssetFetch::default().with_root("resources"),
)),
// Stored asset references for cached asset handles.
image_shader: AssetRef::new("shader://image.shader"),
ferris_texture: AssetRef::new("texture://ferris.png"),
}
}
}
impl AppState<Vertex> for State {
fn on_init(&mut self, graphics: &mut Graphics<Vertex>) {
// Setup scene camera.
graphics.color = [0.25, 0.25, 0.25, 1.0];
graphics.main_camera.screen_alignment = 0.5.into();
graphics.main_camera.scaling = CameraScaling::FitToView {
size: 1000.0.into(),
inside: false,
};
// Load this scene group.
self.assets.ensure("group://ingame.txt").unwrap();
}
fn on_redraw(&mut self, graphics: &mut Graphics<Vertex>) {
// Process assets periotically.
if self.timer.elapsed().as_secs_f32() > DELTA_TIME {
self.timer = Instant::now();
self.process_assets(graphics);
}
// Do not render unless we have shader loaded.
let Ok(image_shader) = self.image_shader.resolve(&self.assets) else {
return;
};
let Some(image_shader) = image_shader
.access_checked::<&AsyncHandle<Shader>>()
.map(|handle| handle.to_ref())
else {
return;
};
// Begin drawing objects.
self.context.begin_frame(graphics);
self.context.push_shader(&image_shader);
self.context.push_blending(GlowBlending::Alpha);
// Draw sprite only if texture asset is loaded.
if let Ok(texture) = self.ferris_texture.resolve(&self.assets) {
if let Some(texture) = texture
.access_checked::<&AsyncHandle<Texture>>()
.map(|handle| handle.to_ref())
{
Sprite::single(SpriteTexture::new("u_image".into(), texture))
.pivot(0.5.into())
.draw(&mut self.context, graphics);
}
}
// Commit drawn objects.
self.context.end_frame();
}
}
impl State {
fn process_assets(&mut self, graphics: &mut Graphics<Vertex>) {
let mut commands = CommandBuffer::default();
// Process loaded shader assets into shader objects on GPU.
for entity in self.assets.storage.added().iter_of::<ShaderAsset>() {
let asset = self
.assets
.storage
.component::<true, ShaderAsset>(entity)
.unwrap();
let shader = graphics.shader(&asset.vertex, &asset.fragment).unwrap();
println!("* Shader asset turned into shader: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(shader),)));
}
// Process loaded texture assets into texture objects on GPU.
for entity in self.assets.storage.added().iter_of::<TextureAsset>() {
let asset = self
.assets
.storage
.component::<true, TextureAsset>(entity)
.unwrap();
let texture = graphics
.texture(
asset.width,
asset.height,
1,
GlowTextureFormat::Rgba,
Some(&asset.bytes),
)
.unwrap();
println!("* Texture asset turned into texture: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(texture),)));
}
commands.execute(&mut self.assets.storage);
self.assets.maintain().unwrap();
}
}
// Workaround for GPU handles not being Send + Sync,
// to be able to store them in assets database.
struct AsyncHandle<T: Clone>(Arc<RwLock<T>>);
unsafe impl<T: Clone> Send for AsyncHandle<T> {}
unsafe impl<T: Clone> Sync for AsyncHandle<T> {}
impl<T: Clone> AsyncHandle<T> {
fn new(data: T) -> Self {
Self(Arc::new(RwLock::new(data)))
}
fn get(&self) -> T {
self.0.read().unwrap().clone()
}
fn to_ref(&self) -> ResourceRef<T> {
ResourceRef::object(self.get())
}
}
// Decoded shader asset information with dependencies.
#[derive(Debug, Serialize, Deserialize)]
struct ShaderAssetInfo {
vertex: AssetRef,
fragment: AssetRef,
}
// Shader asset with vertex and fragment programs code.
struct ShaderAsset {
vertex: String,
fragment: String,
}
// Shader asset processor that turns bytes -> shader info -> shader asset.
struct ShaderAssetProcessor;
impl BundleWithDependenciesProcessor for ShaderAssetProcessor {
type Bundle = (ShaderAssetInfo,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<ShaderAssetInfo>(&bytes)?;
let vertex = asset.vertex.path().clone();
let fragment = asset.fragment.path().clone();
println!("* Shader asset processed: {:#?}", asset);
Ok(BundleWithDependencies::new((asset,))
.dependency(vertex)
.dependency(fragment))
}
fn maintain(&mut self, storage: &mut World) -> Result<(), Box<dyn Error>> {
let mut commands = CommandBuffer::default();
let mut lookup = storage.lookup_access::<true, &String>();
// We scan for decoded shader info and if dependencies are loaded,
// then turn them into shader asset.
for (entity, info, dependencies) in
storage.query::<true, (Entity, &ShaderAssetInfo, &Relation<AssetDependency>)>()
{
if dependencies
.entities()
.all(|entity| storage.has_entity_component::<String>(entity))
{
let vertex = lookup
.access(storage.find_by::<true, _>(info.vertex.path()).unwrap())
.unwrap()
.to_owned();
let fragment = lookup
.access(storage.find_by::<true, _>(info.fragment.path()).unwrap())
.unwrap()
.to_owned();
let asset = ShaderAsset { vertex, fragment };
commands.command(InsertCommand::new(entity, (asset,)));
commands.command(RemoveCommand::<(ShaderAssetInfo,)>::new(entity));
}
}
drop(lookup);
commands.execute(storage);
Ok(())
}
}
// Decoded texture asset with its size and decoded bitmap bytes.
struct TextureAsset {
width: u32,
height: u32,
bytes: Vec<u8>,
}
struct TextureAssetProcessor;
impl BundleWithDependenciesProcessor for TextureAssetProcessor {
type Bundle = (TextureAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
// Decode PNG image into texture size and bitmap bytes.
let decoder = png::Decoder::new(bytes.as_slice());
let mut reader = decoder.read_info()?;
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf)?;
let bytes = buf[..info.buffer_size()].to_vec();
println!("* Texture asset processed: {:#?}", info);
Ok(BundleWithDependencies::new((TextureAsset {
width: info.width,
height: info.height,
bytes,
},)))
}
}
Texture asset processor
Now let's see how we have made our texture asset processor:
use keket::{
database::{handle::AssetDependency, reference::AssetRef, AssetDatabase},
fetch::{deferred::DeferredAssetFetch, file::FileAssetFetch},
protocol::{
bundle::{BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor},
group::GroupAssetProtocol,
text::TextAssetProtocol,
},
third_party::anput::{
commands::{CommandBuffer, InsertCommand, RemoveCommand},
entity::Entity,
world::{Relation, World},
},
};
use serde::{Deserialize, Serialize};
use spitfire::prelude::*;
use std::{
error::Error,
sync::{Arc, RwLock},
time::Instant,
};
fn main() -> Result<(), Box<dyn Error>> {
App::<Vertex>::default().run(State::default());
Ok(())
}
const DELTA_TIME: f32 = 1.0 / 60.0;
struct State {
// We store drawing context for later use in app state.
// Drawing context holds resources and stack-based states.
context: DrawContext,
// Timer used for fixed step frame particle system simulation.
timer: Instant,
assets: AssetDatabase,
image_shader: AssetRef,
ferris_texture: AssetRef,
}
impl Default for State {
fn default() -> Self {
Self {
context: Default::default(),
timer: Instant::now(),
assets: AssetDatabase::default()
// Text protocol for shader sources.
.with_protocol(TextAssetProtocol)
// Group protocol for loading many assets at once.
.with_protocol(GroupAssetProtocol)
// Custom shader protocol.
.with_protocol(BundleAssetProtocol::new("shader", ShaderAssetProcessor))
// Custom texture protocol.
.with_protocol(BundleAssetProtocol::new("texture", TextureAssetProcessor))
// Load all data from file system asynchronously.
.with_fetch(DeferredAssetFetch::new(
FileAssetFetch::default().with_root("resources"),
)),
// Stored asset references for cached asset handles.
image_shader: AssetRef::new("shader://image.shader"),
ferris_texture: AssetRef::new("texture://ferris.png"),
}
}
}
impl AppState<Vertex> for State {
fn on_init(&mut self, graphics: &mut Graphics<Vertex>) {
// Setup scene camera.
graphics.color = [0.25, 0.25, 0.25, 1.0];
graphics.main_camera.screen_alignment = 0.5.into();
graphics.main_camera.scaling = CameraScaling::FitToView {
size: 1000.0.into(),
inside: false,
};
// Load this scene group.
self.assets.ensure("group://ingame.txt").unwrap();
}
fn on_redraw(&mut self, graphics: &mut Graphics<Vertex>) {
// Process assets periotically.
if self.timer.elapsed().as_secs_f32() > DELTA_TIME {
self.timer = Instant::now();
self.process_assets(graphics);
}
// Do not render unless we have shader loaded.
let Ok(image_shader) = self.image_shader.resolve(&self.assets) else {
return;
};
let Some(image_shader) = image_shader
.access_checked::<&AsyncHandle<Shader>>()
.map(|handle| handle.to_ref())
else {
return;
};
// Begin drawing objects.
self.context.begin_frame(graphics);
self.context.push_shader(&image_shader);
self.context.push_blending(GlowBlending::Alpha);
// Draw sprite only if texture asset is loaded.
if let Ok(texture) = self.ferris_texture.resolve(&self.assets) {
if let Some(texture) = texture
.access_checked::<&AsyncHandle<Texture>>()
.map(|handle| handle.to_ref())
{
Sprite::single(SpriteTexture::new("u_image".into(), texture))
.pivot(0.5.into())
.draw(&mut self.context, graphics);
}
}
// Commit drawn objects.
self.context.end_frame();
}
}
impl State {
fn process_assets(&mut self, graphics: &mut Graphics<Vertex>) {
let mut commands = CommandBuffer::default();
// Process loaded shader assets into shader objects on GPU.
for entity in self.assets.storage.added().iter_of::<ShaderAsset>() {
let asset = self
.assets
.storage
.component::<true, ShaderAsset>(entity)
.unwrap();
let shader = graphics.shader(&asset.vertex, &asset.fragment).unwrap();
println!("* Shader asset turned into shader: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(shader),)));
}
// Process loaded texture assets into texture objects on GPU.
for entity in self.assets.storage.added().iter_of::<TextureAsset>() {
let asset = self
.assets
.storage
.component::<true, TextureAsset>(entity)
.unwrap();
let texture = graphics
.texture(
asset.width,
asset.height,
1,
GlowTextureFormat::Rgba,
Some(&asset.bytes),
)
.unwrap();
println!("* Texture asset turned into texture: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(texture),)));
}
commands.execute(&mut self.assets.storage);
self.assets.maintain().unwrap();
}
}
// Workaround for GPU handles not being Send + Sync,
// to be able to store them in assets database.
struct AsyncHandle<T: Clone>(Arc<RwLock<T>>);
unsafe impl<T: Clone> Send for AsyncHandle<T> {}
unsafe impl<T: Clone> Sync for AsyncHandle<T> {}
impl<T: Clone> AsyncHandle<T> {
fn new(data: T) -> Self {
Self(Arc::new(RwLock::new(data)))
}
fn get(&self) -> T {
self.0.read().unwrap().clone()
}
fn to_ref(&self) -> ResourceRef<T> {
ResourceRef::object(self.get())
}
}
// Decoded shader asset information with dependencies.
#[derive(Debug, Serialize, Deserialize)]
struct ShaderAssetInfo {
vertex: AssetRef,
fragment: AssetRef,
}
// Shader asset with vertex and fragment programs code.
struct ShaderAsset {
vertex: String,
fragment: String,
}
// Shader asset processor that turns bytes -> shader info -> shader asset.
struct ShaderAssetProcessor;
impl BundleWithDependenciesProcessor for ShaderAssetProcessor {
type Bundle = (ShaderAssetInfo,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<ShaderAssetInfo>(&bytes)?;
let vertex = asset.vertex.path().clone();
let fragment = asset.fragment.path().clone();
println!("* Shader asset processed: {:#?}", asset);
Ok(BundleWithDependencies::new((asset,))
.dependency(vertex)
.dependency(fragment))
}
fn maintain(&mut self, storage: &mut World) -> Result<(), Box<dyn Error>> {
let mut commands = CommandBuffer::default();
let mut lookup = storage.lookup_access::<true, &String>();
// We scan for decoded shader info and if dependencies are loaded,
// then turn them into shader asset.
for (entity, info, dependencies) in
storage.query::<true, (Entity, &ShaderAssetInfo, &Relation<AssetDependency>)>()
{
if dependencies
.entities()
.all(|entity| storage.has_entity_component::<String>(entity))
{
let vertex = lookup
.access(storage.find_by::<true, _>(info.vertex.path()).unwrap())
.unwrap()
.to_owned();
let fragment = lookup
.access(storage.find_by::<true, _>(info.fragment.path()).unwrap())
.unwrap()
.to_owned();
let asset = ShaderAsset { vertex, fragment };
commands.command(InsertCommand::new(entity, (asset,)));
commands.command(RemoveCommand::<(ShaderAssetInfo,)>::new(entity));
}
}
drop(lookup);
commands.execute(storage);
Ok(())
}
}
// Decoded texture asset with its size and decoded bitmap bytes.
struct TextureAsset {
width: u32,
height: u32,
bytes: Vec<u8>,
}
struct TextureAssetProcessor;
impl BundleWithDependenciesProcessor for TextureAssetProcessor {
type Bundle = (TextureAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
// Decode PNG image into texture size and bitmap bytes.
let decoder = png::Decoder::new(bytes.as_slice());
let mut reader = decoder.read_info()?;
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf)?;
let bytes = buf[..info.buffer_size()].to_vec();
println!("* Texture asset processed: {:#?}", info);
Ok(BundleWithDependencies::new((TextureAsset {
width: info.width,
height: info.height,
bytes,
},)))
}
}
All we do here is we decode PNG image into texture decoded bytes along with texture dimensions.
Shader asset processor
Shader asset processor is a lot more interesting:
use keket::{
database::{handle::AssetDependency, reference::AssetRef, AssetDatabase},
fetch::{deferred::DeferredAssetFetch, file::FileAssetFetch},
protocol::{
bundle::{BundleAssetProtocol, BundleWithDependencies, BundleWithDependenciesProcessor},
group::GroupAssetProtocol,
text::TextAssetProtocol,
},
third_party::anput::{
commands::{CommandBuffer, InsertCommand, RemoveCommand},
entity::Entity,
world::{Relation, World},
},
};
use serde::{Deserialize, Serialize};
use spitfire::prelude::*;
use std::{
error::Error,
sync::{Arc, RwLock},
time::Instant,
};
fn main() -> Result<(), Box<dyn Error>> {
App::<Vertex>::default().run(State::default());
Ok(())
}
const DELTA_TIME: f32 = 1.0 / 60.0;
struct State {
// We store drawing context for later use in app state.
// Drawing context holds resources and stack-based states.
context: DrawContext,
// Timer used for fixed step frame particle system simulation.
timer: Instant,
assets: AssetDatabase,
image_shader: AssetRef,
ferris_texture: AssetRef,
}
impl Default for State {
fn default() -> Self {
Self {
context: Default::default(),
timer: Instant::now(),
assets: AssetDatabase::default()
// Text protocol for shader sources.
.with_protocol(TextAssetProtocol)
// Group protocol for loading many assets at once.
.with_protocol(GroupAssetProtocol)
// Custom shader protocol.
.with_protocol(BundleAssetProtocol::new("shader", ShaderAssetProcessor))
// Custom texture protocol.
.with_protocol(BundleAssetProtocol::new("texture", TextureAssetProcessor))
// Load all data from file system asynchronously.
.with_fetch(DeferredAssetFetch::new(
FileAssetFetch::default().with_root("resources"),
)),
// Stored asset references for cached asset handles.
image_shader: AssetRef::new("shader://image.shader"),
ferris_texture: AssetRef::new("texture://ferris.png"),
}
}
}
impl AppState<Vertex> for State {
fn on_init(&mut self, graphics: &mut Graphics<Vertex>) {
// Setup scene camera.
graphics.color = [0.25, 0.25, 0.25, 1.0];
graphics.main_camera.screen_alignment = 0.5.into();
graphics.main_camera.scaling = CameraScaling::FitToView {
size: 1000.0.into(),
inside: false,
};
// Load this scene group.
self.assets.ensure("group://ingame.txt").unwrap();
}
fn on_redraw(&mut self, graphics: &mut Graphics<Vertex>) {
// Process assets periotically.
if self.timer.elapsed().as_secs_f32() > DELTA_TIME {
self.timer = Instant::now();
self.process_assets(graphics);
}
// Do not render unless we have shader loaded.
let Ok(image_shader) = self.image_shader.resolve(&self.assets) else {
return;
};
let Some(image_shader) = image_shader
.access_checked::<&AsyncHandle<Shader>>()
.map(|handle| handle.to_ref())
else {
return;
};
// Begin drawing objects.
self.context.begin_frame(graphics);
self.context.push_shader(&image_shader);
self.context.push_blending(GlowBlending::Alpha);
// Draw sprite only if texture asset is loaded.
if let Ok(texture) = self.ferris_texture.resolve(&self.assets) {
if let Some(texture) = texture
.access_checked::<&AsyncHandle<Texture>>()
.map(|handle| handle.to_ref())
{
Sprite::single(SpriteTexture::new("u_image".into(), texture))
.pivot(0.5.into())
.draw(&mut self.context, graphics);
}
}
// Commit drawn objects.
self.context.end_frame();
}
}
impl State {
fn process_assets(&mut self, graphics: &mut Graphics<Vertex>) {
let mut commands = CommandBuffer::default();
// Process loaded shader assets into shader objects on GPU.
for entity in self.assets.storage.added().iter_of::<ShaderAsset>() {
let asset = self
.assets
.storage
.component::<true, ShaderAsset>(entity)
.unwrap();
let shader = graphics.shader(&asset.vertex, &asset.fragment).unwrap();
println!("* Shader asset turned into shader: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(shader),)));
}
// Process loaded texture assets into texture objects on GPU.
for entity in self.assets.storage.added().iter_of::<TextureAsset>() {
let asset = self
.assets
.storage
.component::<true, TextureAsset>(entity)
.unwrap();
let texture = graphics
.texture(
asset.width,
asset.height,
1,
GlowTextureFormat::Rgba,
Some(&asset.bytes),
)
.unwrap();
println!("* Texture asset turned into texture: {}", entity);
commands.command(InsertCommand::new(entity, (AsyncHandle::new(texture),)));
}
commands.execute(&mut self.assets.storage);
self.assets.maintain().unwrap();
}
}
// Workaround for GPU handles not being Send + Sync,
// to be able to store them in assets database.
struct AsyncHandle<T: Clone>(Arc<RwLock<T>>);
unsafe impl<T: Clone> Send for AsyncHandle<T> {}
unsafe impl<T: Clone> Sync for AsyncHandle<T> {}
impl<T: Clone> AsyncHandle<T> {
fn new(data: T) -> Self {
Self(Arc::new(RwLock::new(data)))
}
fn get(&self) -> T {
self.0.read().unwrap().clone()
}
fn to_ref(&self) -> ResourceRef<T> {
ResourceRef::object(self.get())
}
}
// Decoded shader asset information with dependencies.
#[derive(Debug, Serialize, Deserialize)]
struct ShaderAssetInfo {
vertex: AssetRef,
fragment: AssetRef,
}
// Shader asset with vertex and fragment programs code.
struct ShaderAsset {
vertex: String,
fragment: String,
}
// Shader asset processor that turns bytes -> shader info -> shader asset.
struct ShaderAssetProcessor;
impl BundleWithDependenciesProcessor for ShaderAssetProcessor {
type Bundle = (ShaderAssetInfo,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
let asset = serde_json::from_slice::<ShaderAssetInfo>(&bytes)?;
let vertex = asset.vertex.path().clone();
let fragment = asset.fragment.path().clone();
println!("* Shader asset processed: {:#?}", asset);
Ok(BundleWithDependencies::new((asset,))
.dependency(vertex)
.dependency(fragment))
}
fn maintain(&mut self, storage: &mut World) -> Result<(), Box<dyn Error>> {
let mut commands = CommandBuffer::default();
let mut lookup = storage.lookup_access::<true, &String>();
// We scan for decoded shader info and if dependencies are loaded,
// then turn them into shader asset.
for (entity, info, dependencies) in
storage.query::<true, (Entity, &ShaderAssetInfo, &Relation<AssetDependency>)>()
{
if dependencies
.entities()
.all(|entity| storage.has_entity_component::<String>(entity))
{
let vertex = lookup
.access(storage.find_by::<true, _>(info.vertex.path()).unwrap())
.unwrap()
.to_owned();
let fragment = lookup
.access(storage.find_by::<true, _>(info.fragment.path()).unwrap())
.unwrap()
.to_owned();
let asset = ShaderAsset { vertex, fragment };
commands.command(InsertCommand::new(entity, (asset,)));
commands.command(RemoveCommand::<(ShaderAssetInfo,)>::new(entity));
}
}
drop(lookup);
commands.execute(storage);
Ok(())
}
}
// Decoded texture asset with its size and decoded bitmap bytes.
struct TextureAsset {
width: u32,
height: u32,
bytes: Vec<u8>,
}
struct TextureAssetProcessor;
impl BundleWithDependenciesProcessor for TextureAssetProcessor {
type Bundle = (TextureAsset,);
fn process_bytes(
&mut self,
bytes: Vec<u8>,
) -> Result<BundleWithDependencies<Self::Bundle>, Box<dyn Error>> {
// Decode PNG image into texture size and bitmap bytes.
let decoder = png::Decoder::new(bytes.as_slice());
let mut reader = decoder.read_info()?;
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf)?;
let bytes = buf[..info.buffer_size()].to_vec();
println!("* Texture asset processed: {:#?}", info);
Ok(BundleWithDependencies::new((TextureAsset {
width: info.width,
height: info.height,
bytes,
},)))
}
}
Since this asset is just a descriptor that holds references to dependencies such
as vertex anf fragment shader code, in asset processing we only schedule
dependency loading and then in maintain
method we scan database storage for
ShaderAssetInfo
component, along with dependency relations, and we test if all
dependencies are resolved (they are text assets so all should have String
component when complete). When all dependencies are complete, we can read them
and construct ShaderAsset
with shader programs code for game to build GPU
shader objects.