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.