tirbofish/dropbear · commit
aa156aecdea0f1acda59c4f1a2195bb006d4e01f
by far the biggest change in this repository. here are the changes:
firstly, i tried to work on WASM, but it kept bothering me that there is still no async loading (i hate when it blocks), so I decided to fix that.
firstly, i created a LazyType trait which defines a struct that can be lazy. really, its that it does CPU intensive function at the start (like in another thread), then does the graphics related stuff in another thread.
but before that, i realised that the graphics crate is non send+sync, so I have been working on using Arcs to separate between the frame changing data and the shared "clonable" data. State is still the source of truth.
i have managed to optimise it pretty well. i am curious about the LOC of code i have changed/refactored.
also "technically" i have started on gleam, but i cannot really say much.
Signature present but could not be verified.Unverified
@@ -22,4 +22,10 @@ target Cargo.lock dropbear-engine/src/resources/textures/Autism.png .vscode -docs +docs + +# gleam +*.beam +*.ez +/build +erl_crash.dump @@ -6,7 +6,9 @@ package.repository = "https://github.com/4tkbytes/dropbear-engine" package.readme = "README.md" resolver = "3" -members = ["dropbear-engine", "eucalyptus-core", "eucalyptus-editor", "redback-runtime"] +members = ["dropbear-engine", "eucalyptus-core", "eucalyptus-editor"] +# members = ["dropbear-engine", "eucalyptus-core", "eucalyptus-editor", "redback-runtime"] + [workspace.dependencies] anyhow = { version = "1.0", features = ["backtrace"] } @@ -52,6 +54,10 @@ zip = "5.1" walkdir = "2.3" wasmer = { version = "6.1.0-rc.3" } rayon = "1.11" +flate2 = "1.1" +reqwest = { version = "0.11", features = ["stream"] } +tar = "0.4" +async-trait = "0.1" gltf = "1" thiserror = "2.0" @@ -34,6 +34,8 @@ parking_lot.workspace = true lazy_static.workspace = true gltf.workspace = true rayon.workspace = true +tokio.workspace = true +async-trait.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true @@ -1,10 +1,12 @@ +use std::sync::Arc; + use glam::{DMat4, DQuat, DVec3, Mat4}; use wgpu::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, Buffer, BufferBindingType, ShaderStages, }; -use crate::graphics::Graphics; +use crate::graphics::SharedGraphicsContext; #[rustfmt::skip] pub const OPENGL_TO_WGPU_MATRIX: [[f64; 4]; 4] = [ @@ -44,7 +46,7 @@ pub struct Camera { impl Camera { /// Creates a new camera pub fn new( - graphics: &Graphics, + graphics: Arc<SharedGraphicsContext>, eye: DVec3, target: DVec3, up: DVec3, @@ -82,20 +84,20 @@ impl Camera { }; camera.update_view_proj(); let buffer = graphics.create_uniform(camera.uniform, Some("Camera Uniform")); - camera.create_bind_group_layout(graphics, buffer.clone()); + camera.create_bind_group_layout(graphics.clone(), buffer.clone()); camera.buffer = Some(buffer); log::debug!("Created new camera"); camera } /// Creates a default camera - pub fn predetermined(graphics: &Graphics, label: Option<&str>) -> Self { + pub fn predetermined(graphics: Arc<SharedGraphicsContext>, label: Option<&str>) -> Self { Self::new( - graphics, + graphics.clone(), DVec3::new(0.0, 1.0, 2.0), DVec3::new(0.0, 0.0, 0.0), DVec3::Y, - (graphics.state.config.width / graphics.state.config.height).into(), + (graphics.screen_size.0 / graphics.screen_size.1).into(), 45.0, 0.1, 100.0, @@ -159,10 +161,9 @@ impl Camera { result } - pub fn create_bind_group_layout(&mut self, graphics: &Graphics, camera_buffer: Buffer) { + pub fn create_bind_group_layout(&mut self, graphics: Arc<SharedGraphicsContext>, camera_buffer: Buffer) { let camera_bind_group_layout = graphics - .state .device .create_bind_group_layout(&BindGroupLayoutDescriptor { entries: &[BindGroupLayoutEntry { @@ -179,7 +180,6 @@ impl Camera { }); let camera_bind_group = graphics - .state .device .create_bind_group(&BindGroupDescriptor { layout: &camera_bind_group_layout, @@ -193,9 +193,9 @@ impl Camera { self.bind_group = Some(camera_bind_group); } - pub fn update(&mut self, graphics: &Graphics) { + pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>) { self.update_view_proj(); - graphics.state.queue.write_buffer( + graphics.queue.write_buffer( &self.buffer.as_ref().unwrap(), 0, bytemuck::cast_slice(&[self.uniform]), @@ -1,11 +1,12 @@ +use futures::executor::block_on; use glam::{DMat4, DQuat, DVec3, Mat4}; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use wgpu::{Buffer, util::DeviceExt}; use crate::{ - graphics::{Graphics, Instance}, - model::Model, + graphics::{SharedGraphicsContext, Instance}, + model::{LazyModel, LazyType, Model}, }; #[derive(Debug, Clone, Deserialize, Serialize, Copy, PartialEq)] @@ -55,6 +56,55 @@ impl Transform { } } +/// Creates a new adopted entity in a lazy method. It fetches the data first (which can be done on a separate +/// thread). After, the [`LazyAdoptedEntity::poke()`] function can be called to convert the Lazy to a Real adopted entity. +#[derive(Default)] +pub struct LazyAdoptedEntity { + lazy_model: LazyModel, + #[allow(dead_code)] + label: String, +} + +impl LazyAdoptedEntity { + /// Create a LazyAdoptedEntity from a file path (can be run on background thread) + pub async fn from_file(path: &PathBuf, label: Option<&str>) -> anyhow::Result<Self> { + let buffer = std::fs::read(path)?; + Self::from_memory(buffer, label).await + } + + /// Create a LazyAdoptedEntity from memory buffer (can be run on background thread) + pub async fn from_memory( + buffer: impl AsRef<[u8]>, + label: Option<&str>, + ) -> anyhow::Result<Self> { + let lazy_model = Model::lazy_load(buffer, label).await?; + let label_str = label.unwrap_or("LazyAdoptedEntity").to_string(); + + Ok(Self { + lazy_model, + label: label_str, + }) + } + + /// Create a LazyAdoptedEntity from an existing LazyModel + pub fn from_lazy_model(lazy_model: LazyModel, label: Option<&str>) -> Self { + let label_str = label.unwrap_or("LazyAdoptedEntity").to_string(); + Self { + lazy_model, + label: label_str, + } + } +} + +impl LazyType for LazyAdoptedEntity { + type T = AdoptedEntity; + + fn poke(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<Self::T> { + let model = self.lazy_model.poke(graphics.clone())?; + Ok(block_on(AdoptedEntity::adopt(graphics, model))) + } +} + #[derive(Default)] pub struct AdoptedEntity { pub model: Option<Model>, @@ -64,9 +114,9 @@ pub struct AdoptedEntity { } impl AdoptedEntity { - pub fn new(graphics: &Graphics, path: &PathBuf, label: Option<&str>) -> anyhow::Result<Self> { - let model = Model::load(graphics, path, label.clone())?; - Ok(Self::adopt(graphics, model, label)) + pub async fn new(graphics: Arc<SharedGraphicsContext>, path: &PathBuf, label: Option<&str>) -> anyhow::Result<Self> { + let model = Model::load(graphics.clone(), path, label.clone()).await?; + Ok(Self::adopt(graphics, model).await) } pub fn label(&self) -> &String { @@ -81,19 +131,16 @@ impl AdoptedEntity { self.model_mut().label = label.to_string(); } - pub fn adopt(graphics: &Graphics, model: Model, label: Option<&str>) -> Self { + pub async fn adopt(graphics: Arc<SharedGraphicsContext>, model: Model) -> Self { + let label = model.label.clone(); let instance = Instance::new(DVec3::ZERO, DQuat::IDENTITY, DVec3::ONE); let initial_matrix = DMat4::IDENTITY; // Default; update in new() if transform provided let instance_raw = instance.to_raw(); let instance_buffer = graphics - .state .device .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: match label { - Some(l) => Some(l), - None => Some("instance buffer"), - }, + label: Some(&label), contents: bytemuck::cast_slice(&[instance_raw]), usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, }); @@ -106,14 +153,13 @@ impl AdoptedEntity { } } - pub fn update(&mut self, graphics: &Graphics, transform: &Transform) { + pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>, transform: &Transform) { let current_matrix = transform.matrix(); if self.previous_matrix != current_matrix { self.instance = Instance::from_matrix(current_matrix); let instance_raw = self.instance.to_raw(); if let Some(buffer) = &self.instance_buffer { graphics - .state .queue .write_buffer(buffer, 0, bytemuck::cast_slice(&[instance_raw])); } @@ -1,42 +1,89 @@ -use std::{fs, path::PathBuf, time::Instant}; +use std::{fs, path::PathBuf, sync::Arc, time::Instant}; -use egui::Context; +use egui::{Context, TextureId}; use glam::{DMat4, DQuat, DVec3, Mat3}; use image::GenericImageView; +use parking_lot::Mutex; // use nalgebra::{Matrix4, UnitQuaternion, Vector3}; use wgpu::{ - BindGroup, BindGroupLayout, Buffer, BufferAddress, BufferUsages, Color, CommandEncoder, - CompareFunction, DepthBiasState, Device, Extent3d, LoadOp, Operations, RenderPass, - RenderPassDepthStencilAttachment, RenderPipeline, Sampler, ShaderModule, StencilState, - SurfaceConfiguration, TextureDescriptor, TextureFormat, TextureUsages, TextureView, - TextureViewDescriptor, VertexBufferLayout, - util::{BufferInitDescriptor, DeviceExt}, + util::{BufferInitDescriptor, DeviceExt}, BindGroup, BindGroupLayout, Buffer, BufferAddress, BufferUsages, Color, CommandEncoder, CompareFunction, DepthBiasState, Device, Extent3d, LoadOp, Operations, Queue, RenderPass, RenderPassDepthStencilAttachment, RenderPipeline, Sampler, ShaderModule, StencilState, SurfaceConfiguration, TextureDescriptor, TextureFormat, TextureUsages, TextureView, TextureViewDescriptor, VertexBufferLayout }; +use winit::window::Window; use crate::{ - State, - model::{self, Vertex}, + egui_renderer::EguiRenderer, model::{self, Vertex}, State }; -pub struct Graphics<'a> { - pub state: &'a mut State, - pub view: &'a TextureView, +pub const NO_TEXTURE: &'static [u8] = include_bytes!("../../resources/no-texture.png"); +pub const NO_MODEL: &'static [u8] = include_bytes!("../../resources/error.glb"); + +pub struct RenderContext<'a> { + pub shared: Arc<SharedGraphicsContext>, + pub frame: FrameGraphicsContext<'a>, +} + +pub struct SharedGraphicsContext { + pub device: Arc<Device>, + pub queue: Arc<Queue>, + pub instance: Arc<wgpu::Instance>, + pub texture_bind_layout: Arc<BindGroupLayout>, + pub window: Arc<Window>, + pub viewport_texture: Arc<Texture>, + pub egui_renderer: Arc<Mutex<EguiRenderer>>, + pub diffuse_sampler: Arc<Sampler>, + pub screen_size: (f32, f32), + pub texture_id: Arc<TextureId>, +} + +pub struct FrameGraphicsContext<'a> { pub encoder: &'a mut CommandEncoder, + pub view: &'a TextureView, + pub depth_texture: &'a Texture, pub screen_size: (f32, f32), - pub diffuse_sampler: Sampler, } -pub const NO_TEXTURE: &'static [u8] = include_bytes!("../../resources/no-texture.png"); -pub const NO_MODEL: &'static [u8] = include_bytes!("../../resources/error.glb"); +impl SharedGraphicsContext { + pub fn get_egui_context(&self) -> Context { + self.egui_renderer.lock().context() + } + + pub fn create_uniform<T>(&self, uniform: T, label: Option<&str>) -> Buffer + where + T: bytemuck::Pod + bytemuck::Zeroable, + { + self.device.create_buffer_init(&BufferInitDescriptor { + label, + contents: bytemuck::cast_slice(&[uniform]), + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + }) + } -impl<'a> Graphics<'a> { - pub fn new( + pub fn create_model_uniform_bind_group_layout(&self) -> BindGroupLayout { + self.device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: Some("model_uniform_bind_group_layout"), + }) + } +} + +impl<'a> RenderContext<'a> { + pub fn from_state( state: &'a mut State, view: &'a TextureView, encoder: &'a mut CommandEncoder, ) -> Self { let screen_size = (state.config.width as f32, state.config.height as f32); - let diffuse_sampler = state + let diffuse_sampler = Arc::new(state .device .create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::ClampToEdge, @@ -46,33 +93,37 @@ impl<'a> Graphics<'a> { min_filter: wgpu::FilterMode::Nearest, mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() - }); + })); Self { - state, - view, - encoder, - screen_size, - diffuse_sampler, + shared: Arc::new(SharedGraphicsContext { + device: state.device.clone(), + queue: state.queue.clone(), + instance: Arc::new(state.instance.clone()), + texture_bind_layout: Arc::new(state.texture_bind_layout.clone()), + window: state.window.clone(), + viewport_texture: Arc::new(state.viewport_texture.clone()), + egui_renderer: state.egui_renderer.clone(), + diffuse_sampler, + screen_size, + texture_id: state.texture_id.clone(), + }), + frame: FrameGraphicsContext { + encoder, + view, + depth_texture: &state.depth_texture, + screen_size, + }, } } - pub fn resize(&mut self, width: u32, height: u32) { - self.state.resize(width, height); - } - - pub fn texture_bind_group(&mut self) -> &wgpu::BindGroupLayout { - &self.state.texture_bind_layout - } - pub fn create_render_pipline( - &mut self, + &self, shader: &Shader, bind_group_layouts: Vec<&BindGroupLayout>, label: Option<&str>, ) -> RenderPipeline { let render_pipeline_layout = - self.state - .device + self.shared.device .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some(label.unwrap_or("Render Pipeline Descriptor")), bind_group_layouts: bind_group_layouts.as_slice(), @@ -80,8 +131,7 @@ impl<'a> Graphics<'a> { }); let render_pipeline = - self.state - .device + self.shared.device .create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some(label.unwrap_or("Render Pipeline")), layout: Some(&render_pipeline_layout), @@ -130,16 +180,12 @@ impl<'a> Graphics<'a> { render_pipeline } - pub fn get_egui_context(&mut self) -> Context { - self.state.egui_renderer.context() - } - pub fn clear_colour(&mut self, color: Color) -> RenderPass<'static> { - self.encoder + self.frame.encoder .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: &self.view, + view: &self.frame.view, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(color), @@ -148,7 +194,7 @@ impl<'a> Graphics<'a> { depth_slice: None, })], depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &self.state.depth_texture.view, + view: &self.frame.depth_texture.view, depth_ops: Some(Operations { load: LoadOp::Clear(0.0), store: wgpu::StoreOp::Store, @@ -160,46 +206,17 @@ impl<'a> Graphics<'a> { }) .forget_lifetime() } - - pub fn create_uniform<T>(&self, uniform: T, label: Option<&str>) -> Buffer - where - T: bytemuck::Pod + bytemuck::Zeroable, - { - self.state.device.create_buffer_init(&BufferInitDescriptor { - label, - contents: bytemuck::cast_slice(&[uniform]), - usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, - }) - } - - pub fn create_model_uniform_bind_group_layout(&self) -> BindGroupLayout { - self.state - .device - .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - label: Some("model_uniform_bind_group_layout"), - }) - } } +// A nice little struct that stored basic information about a WGPU shader. pub struct Shader { pub label: String, pub module: ShaderModule, } impl Shader { - pub fn new(graphics: &Graphics, shader_file_contents: &str, label: Option<&str>) -> Self { + pub fn new(graphics: Arc<SharedGraphicsContext>, shader_file_contents: &str, label: Option<&str>) -> Self { let module = graphics - .state .device .create_shader_module(wgpu::ShaderModuleDescriptor { label, @@ -219,18 +236,119 @@ impl Shader { } #[derive(Clone)] +/// Describes a texture, like an image of some sort. Can be a normal texture on a model or a viewport or depth texture. pub struct Texture { pub texture: wgpu::Texture, pub sampler: Sampler, pub size: Extent3d, pub view: TextureView, pub bind_group: Option<BindGroup>, - pub layout: Option<BindGroupLayout>, + pub layout: Option<Arc<BindGroupLayout>>, } impl Texture { + /// Describes the depth format for all Texture related functions in WGPU to use. Makes life easier pub const DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float; + /// Creates a new Texture from the bytes of an image. This function is blocking, and takes roughly 4 seconds to + /// convert from the image to RGBA, which can cause issues. There are better options, such as doing it yourself. + /// + /// Once async is implemented, this will be a better use. + pub fn new(graphics: Arc<SharedGraphicsContext>, diffuse_bytes: &[u8]) -> Self { + let start = Instant::now(); + let diffuse_image = image::load_from_memory(diffuse_bytes).unwrap(); + println!("Loading image to memory: {:?}", start.elapsed()); + + let start = Instant::now(); + let diffuse_rgba = diffuse_image.to_rgba8(); + println!("Converting diffuse image to rgba8 took {:?}", start.elapsed()); + + let dimensions = diffuse_image.dimensions(); + let texture_size = wgpu::Extent3d { + width: dimensions.0, + height: dimensions.1, + depth_or_array_layers: 1, + }; + + let start = Instant::now(); + let diffuse_texture = graphics + .device + .create_texture(&wgpu::TextureDescriptor { + label: Some("diffuse_texture"), + size: texture_size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], + }); + println!("Creating new diffuse texture took {:?}", start.elapsed()); + + let start = Instant::now(); + graphics.queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &diffuse_texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &diffuse_rgba, + wgpu::TexelCopyBufferLayout { + offset: 0, + bytes_per_row: Some(4 * dimensions.0), + rows_per_image: Some(dimensions.1), + }, + texture_size, + ); + println!("Writing texture to graphics queue took {:?}", start.elapsed()); + + let start = Instant::now(); + let diffuse_texture_view = diffuse_texture.create_view(&TextureViewDescriptor::default()); + let diffuse_sampler = graphics + .device + .create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + println!("Creating sampler took {:?}", start.elapsed()); + + let start = Instant::now(); + let diffuse_bind_group = + graphics + .device + .create_bind_group(&wgpu::BindGroupDescriptor { + layout: &graphics.texture_bind_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&diffuse_texture_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&diffuse_sampler), + }, + ], + label: Some("texture_bind_group"), + }); + println!("Creating diffuse bind group took {:?}", start.elapsed()); + println!("Done creating texture"); + Self { + bind_group: Some(diffuse_bind_group), + layout: Some(graphics.texture_bind_layout.clone()), + texture: diffuse_texture, + sampler: diffuse_sampler, + size: texture_size, + view: diffuse_texture_view, + } + } + + /// Creates a new depth texture. This is an internal function. pub fn create_depth_texture( config: &SurfaceConfiguration, device: &Device, @@ -278,6 +396,7 @@ impl Texture { } } + /// Creates a viewport texture. This is an internal function. pub fn create_viewport_texture( config: &SurfaceConfiguration, device: &Device, @@ -314,16 +433,20 @@ impl Texture { } } + /// Returns a reference to the bind group layout of that texture pub fn layout(&self) -> &BindGroupLayout { self.layout.as_ref().unwrap() } + /// Returns a reference to the bind group of that texture pub fn bind_group(&self) -> &BindGroup { self.bind_group.as_ref().unwrap() } - pub(crate) fn create_texture_from_rgba_data( - graphics: &Graphics, + /// Alternative to [`Texture::new()`], which uses an existing rgba data buffer compared to new which synchronously + /// converts the image to RGBA form. + pub(crate) fn from_rgba_buffer( + graphics: Arc<SharedGraphicsContext>, rgba_data: &[u8], dimensions: (u32, u32), ) -> Texture { @@ -334,7 +457,7 @@ impl Texture { }; let create_start = Instant::now(); - let diffuse_texture = graphics.state.device.create_texture(&wgpu::TextureDescriptor { + let diffuse_texture = graphics.device.create_texture(&wgpu::TextureDescriptor { label: Some("diffuse_texture"), size: texture_size, mip_level_count: 1, @@ -347,7 +470,7 @@ impl Texture { println!("Creating new diffuse texture took {:?}", create_start.elapsed()); let write_start = Instant::now(); - graphics.state.queue.write_texture( + graphics.queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &diffuse_texture, mip_level: 0, @@ -365,7 +488,7 @@ impl Texture { println!("Writing texture to graphics queue took {:?}", write_start.elapsed()); let sampler_start = Instant::now(); - let diffuse_sampler = graphics.state.device.create_sampler(&wgpu::SamplerDescriptor { + let diffuse_sampler = graphics.device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, @@ -379,8 +502,8 @@ impl Texture { let view = diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default()); let bind_group_start = Instant::now(); - let diffuse_bind_group = graphics.state.device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &graphics.state.texture_bind_layout, + let diffuse_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &graphics.texture_bind_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, @@ -403,51 +526,45 @@ impl Texture { view, size: texture_size, bind_group: Some(diffuse_bind_group), - layout: Some(graphics.state.texture_bind_layout.clone()), + layout: Some(graphics.texture_bind_layout.clone()), } } - pub fn new(graphics: &Graphics, diffuse_bytes: &[u8]) -> Self { - let start = Instant::now(); - let diffuse_image = image::load_from_memory(diffuse_bytes).unwrap(); - println!("Loading image to memory: {:?}", start.elapsed()); - - let start = Instant::now(); - let diffuse_rgba = diffuse_image.to_rgba8(); - println!("Converting diffuse image to rgba8 took {:?}", start.elapsed()); - - let dimensions = diffuse_image.dimensions(); + /// Creates a new [`Texture`] with a specified sampler (wgpu) and already converted RGBA byte buffer. + pub fn new_with_sampler_with_rgba_buffer( + graphics: Arc<SharedGraphicsContext>, + rgba_data: &[u8], + dimensions: (u32, u32), + address_mode: wgpu::AddressMode, + ) -> Self { let texture_size = wgpu::Extent3d { width: dimensions.0, height: dimensions.1, depth_or_array_layers: 1, }; - let start = Instant::now(); - let diffuse_texture = graphics - .state - .device - .create_texture(&wgpu::TextureDescriptor { - label: Some("diffuse_texture"), - size: texture_size, - mip_level_count: 1, - sample_count: 1, - dimension: wgpu::TextureDimension::D2, - format: wgpu::TextureFormat::Rgba8Unorm, - usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, - view_formats: &[], - }); - println!("Creating new diffuse texture took {:?}", start.elapsed()); + let create_start = Instant::now(); + let diffuse_texture = graphics.device.create_texture(&wgpu::TextureDescriptor { + label: Some("diffuse_texture"), + size: texture_size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + println!("Creating new diffuse texture took {:?}", create_start.elapsed()); - let start = Instant::now(); - graphics.state.queue.write_texture( + let write_start = Instant::now(); + graphics.queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &diffuse_texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, - &diffuse_rgba, + rgba_data, wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(4 * dimensions.0), @@ -455,57 +572,59 @@ impl Texture { }, texture_size, ); - println!("Writing texture to graphics queue took {:?}", start.elapsed()); + println!("Writing texture to graphics queue took {:?}", write_start.elapsed()); - let start = Instant::now(); - let diffuse_texture_view = diffuse_texture.create_view(&TextureViewDescriptor::default()); + let sampler_start = Instant::now(); let diffuse_sampler = graphics - .state .device .create_sampler(&wgpu::SamplerDescriptor { - address_mode_u: wgpu::AddressMode::ClampToEdge, - address_mode_v: wgpu::AddressMode::ClampToEdge, - address_mode_w: wgpu::AddressMode::ClampToEdge, + address_mode_u: address_mode, + address_mode_v: address_mode, + address_mode_w: address_mode, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Nearest, mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() - }); - println!("Creating sampler took {:?}", start.elapsed()); + }); + println!("Creating sampler took {:?}", sampler_start.elapsed()); + + let view = diffuse_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let bind_group_start = Instant::now(); + let diffuse_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &graphics.texture_bind_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&diffuse_sampler), + }, + ], + label: Some("texture_bind_group"), + }); + println!("Creating diffuse bind group took {:?}", bind_group_start.elapsed()); - let start = Instant::now(); - let diffuse_bind_group = - graphics - .state - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - layout: &graphics.state.texture_bind_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView(&diffuse_texture_view), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&diffuse_sampler), - }, - ], - label: Some("texture_bind_group"), - }); - println!("Creating diffuse bind group took {:?}", start.elapsed()); println!("Done creating texture"); - Self { - bind_group: Some(diffuse_bind_group), - layout: Some(graphics.state.texture_bind_layout.clone()), + + Texture { texture: diffuse_texture, sampler: diffuse_sampler, + view, size: texture_size, - view: diffuse_texture_view, + bind_group: Some(diffuse_bind_group), + layout: Some(graphics.texture_bind_layout.clone()), } } + /// Creates a new [`Texture`] with a specified sampler (wgpu). + /// + /// This function decodes the image to RGBA, which can take a long time. This function is not + /// recommended to be used until you have async working. pub fn new_with_sampler( - graphics: &Graphics, + graphics: Arc<SharedGraphicsContext>, diffuse_bytes: &[u8], address_mode: wgpu::AddressMode, ) -> Self { @@ -520,7 +639,6 @@ impl Texture { }; let diffuse_texture = graphics - .state .device .create_texture(&wgpu::TextureDescriptor { label: Some("diffuse_texture"), @@ -533,7 +651,7 @@ impl Texture { view_formats: &[], }); - graphics.state.queue.write_texture( + graphics.queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &diffuse_texture, mip_level: 0, @@ -551,7 +669,6 @@ impl Texture { let diffuse_texture_view = diffuse_texture.create_view(&TextureViewDescriptor::default()); let diffuse_sampler = graphics - .state .device .create_sampler(&wgpu::SamplerDescriptor { address_mode_u: address_mode, @@ -565,10 +682,9 @@ impl Texture { let diffuse_bind_group = graphics - .state .device .create_bind_group(&wgpu::BindGroupDescriptor { - layout: &graphics.state.texture_bind_layout, + layout: &graphics.texture_bind_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, @@ -588,13 +704,14 @@ impl Texture { view: diffuse_texture_view, size: texture_size, bind_group: Some(diffuse_bind_group), - layout: Some(graphics.state.texture_bind_layout.clone()), + layout: Some(graphics.texture_bind_layout.clone()), } } - pub async fn load_texture(graphics: &Graphics<'_>, path: &PathBuf) -> anyhow::Result<Texture> { + /// A helper function that loads the texture from a path. Still returns the same [`Texture`]. + pub async fn load_texture(graphics: Arc<SharedGraphicsContext>, path: &PathBuf) -> anyhow::Result<Texture> { let data = fs::read(path)?; - Ok(Self::new(graphics, &data)) + Ok(Self::new(graphics.clone(), &data)) } } @@ -0,0 +1,182 @@ +// This is the graveyard, where old code go to die +// They either served me for a long time, but outgrew their clothing, or they were useless and are there temporarily. +// This mainly exists in the case that there is some code I could reference. + +pub struct Graphics<'a> { + pub state: &'a mut State, + pub view: &'a TextureView, + pub encoder: &'a mut CommandEncoder, + pub screen_size: (f32, f32), + pub diffuse_sampler: Sampler, +} + +impl<'a> Graphics<'a> { + pub fn new( + state: &'a mut State, + view: &'a TextureView, + encoder: &'a mut CommandEncoder, + ) -> Self { + let screen_size = (state.config.width as f32, state.config.height as f32); + let diffuse_sampler = state + .device + .create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + Self { + state, + view, + encoder, + screen_size, + diffuse_sampler, + } + } + + /// Fetches the [`FrameGraphicsContext`], which may be required for certain functions. + pub fn frame_context(&self) -> FrameGraphicsContext { + FrameGraphicsContext { + device: self.state.device.clone(), + queue: self.state.queue.clone(), + texture_bind_layout: Arc::new(self.state.texture_bind_layout.clone()), + } + } + + pub fn resize(&mut self, width: u32, height: u32) { + self.state.resize(width, height); + } + + pub fn texture_bind_group(&mut self) -> &wgpu::BindGroupLayout { + &self.state.texture_bind_layout + } + + pub fn create_render_pipline( + &mut self, + shader: &Shader, + bind_group_layouts: Vec<&BindGroupLayout>, + label: Option<&str>, + ) -> RenderPipeline { + let render_pipeline_layout = + self.state + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some(label.unwrap_or("Render Pipeline Descriptor")), + bind_group_layouts: bind_group_layouts.as_slice(), + push_constant_ranges: &[], + }); + + let render_pipeline = + self.state + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some(label.unwrap_or("Render Pipeline")), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader.module, + entry_point: Some("vs_main"), + buffers: &[model::ModelVertex::desc(), InstanceRaw::desc()], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader.module, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: wgpu::TextureFormat::Rgba8Unorm, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + // cull_mode: Some(wgpu::Face::Back), // todo: change for improved performance + cull_mode: None, + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: Texture::DEPTH_FORMAT, + depth_write_enabled: true, + depth_compare: CompareFunction::Greater, + stencil: StencilState::default(), + bias: DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }); + log::debug!("Created new render pipeline"); + render_pipeline + } + + pub fn get_egui_context(&mut self) -> Context { + self.state.egui_renderer.context() + } + + pub fn clear_colour(&mut self, color: Color) -> RenderPass<'static> { + self.encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &self.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(color), + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &self.state.depth_texture.view, + depth_ops: Some(Operations { + load: LoadOp::Clear(0.0), + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + occlusion_query_set: None, + timestamp_writes: None, + }) + .forget_lifetime() + } + + pub fn create_uniform<T>(&self, uniform: T, label: Option<&str>) -> Buffer + where + T: bytemuck::Pod + bytemuck::Zeroable, + { + self.state.device.create_buffer_init(&BufferInitDescriptor { + label, + contents: bytemuck::cast_slice(&[uniform]), + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + }) + } + + pub fn create_model_uniform_bind_group_layout(&self) -> BindGroupLayout { + self.state + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: Some("model_uniform_bind_group_layout"), + }) + } +} @@ -21,10 +21,10 @@ use egui_wgpu_backend::ScreenDescriptor; use env_logger::Builder; use futures::executor::block_on; use gilrs::{Gilrs, GilrsBuilder}; +use parking_lot::Mutex; use spin_sleep::SpinSleeper; use std::fs::OpenOptions; use std::io::Write; -use std::sync::Mutex; use std::{ fmt::{self, Display, Formatter}, sync::Arc, @@ -46,7 +46,7 @@ use winit::{ use crate::{ egui_renderer::EguiRenderer, - graphics::{Graphics, Texture}, + graphics::Texture, }; pub use gilrs; @@ -57,16 +57,16 @@ pub use winit; /// The backend information, such as the device, queue, config, surface, renderer, window and more. pub struct State { pub surface: Surface<'static>, - pub device: Device, - pub queue: Queue, + pub device: Arc<Device>, + pub queue: Arc<Queue>, pub config: SurfaceConfiguration, pub is_surface_configured: bool, pub depth_texture: Texture, pub texture_bind_layout: BindGroupLayout, - pub egui_renderer: EguiRenderer, + pub egui_renderer: Arc<Mutex<EguiRenderer>>, pub instance: Instance, pub viewport_texture: Texture, - pub texture_id: TextureId, + pub texture_id: Arc<TextureId>, pub window: Arc<Window>, } @@ -173,9 +173,9 @@ Hardware: label: Some("texture_bind_group_layout"), }); - let mut egui_renderer = EguiRenderer::new(&device, config.format, 1, &window); + let mut egui_renderer = Arc::new(Mutex::new(EguiRenderer::new(&device, config.format, 1, &window))); - let texture_id = egui_renderer.renderer().egui_texture_from_wgpu_texture( + let texture_id = Arc::get_mut(&mut egui_renderer).unwrap().lock().renderer().egui_texture_from_wgpu_texture( &device, &viewport_texture.view, wgpu::FilterMode::Linear, @@ -183,8 +183,8 @@ Hardware: let result = Self { surface, - device, - queue, + device: Arc::new(device), + queue: Arc::new(queue), config, is_surface_configured: true, depth_texture, @@ -193,7 +193,7 @@ Hardware: instance, egui_renderer, viewport_texture, - texture_id, + texture_id: Arc::new(texture_id), }; Ok(result) @@ -212,14 +212,15 @@ Hardware: Texture::create_depth_texture(&self.config, &self.device, Some("depth texture")); self.viewport_texture = Texture::create_viewport_texture(&self.config, &self.device, Some("viewport texture")); - self.texture_id = self - .egui_renderer + self.texture_id = Arc::get_mut(&mut self + .egui_renderer).unwrap().lock() .renderer() .egui_texture_from_wgpu_texture( &self.device, &self.viewport_texture.view, wgpu::FilterMode::Linear, - ); + ) + .into(); } /// Asynchronously renders the scene and the egui renderer. I don't know what else to say. @@ -279,16 +280,16 @@ Hardware: let viewport_view = { &self.viewport_texture.view.clone() }; - self.egui_renderer.begin_frame(); - - let mut graphics = Graphics::new(self, viewport_view, &mut encoder); + self.egui_renderer.lock().begin_frame(); + let mut graphics = graphics::RenderContext::from_state(self, viewport_view, &mut encoder); + scene_manager .update(previous_dt, &mut graphics, event_loop) .await; scene_manager.render(&mut graphics).await; - self.egui_renderer.end_frame_and_draw( + self.egui_renderer.lock().end_frame_and_draw( &self.device, &self.queue, &mut encoder, @@ -472,9 +473,9 @@ impl App { write!(buf, "{}", console_line)?; - if let Ok(mut fh) = file.lock() { - let _ = fh.write_all(file_line.as_bytes()); - } + let mut fh = file.lock(); + let _ = fh.write_all(file_line.as_bytes()); + Ok(()) }) .filter(Some("dropbear_engine"), LevelFilter::Trace) @@ -561,7 +562,7 @@ impl ApplicationHandler for App { None => return, }; - state.egui_renderer.handle_input(&event); + state.egui_renderer.lock().handle_input(&event); match event { WindowEvent::CloseRequested => { @@ -1,15 +1,18 @@ use glam::{DMat4, DQuat, DVec3}; use std::fmt::{Display, Formatter}; +use std::sync::Arc; use wgpu::{ BindGroup, BindGroupLayout, Buffer, BufferAddress, CompareFunction, DepthBiasState, RenderPipeline, StencilState, VertexBufferLayout, util::DeviceExt, }; use crate::attenuation::{Attenuation, RANGE_50}; +use crate::graphics::SharedGraphicsContext; +use crate::model::{LazyModel, LazyType}; use crate::{ camera::Camera, entity::Transform, - graphics::{Graphics, Shader}, + graphics::Shader, model::{self, Model, Vertex}, }; @@ -197,6 +200,92 @@ impl LightComponent { } } +pub struct LazyLight { + light_component: LightComponent, + transform: Transform, + label: Option<String>, + cube_lazy_model: Option<LazyModel>, +} + +impl LazyType for LazyLight { + type T = Light; + + fn poke(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<Self::T> { + let label_str = self.label.clone().unwrap_or_else(|| "Light".to_string()); + + let forward = DVec3::new(0.0, 0.0, -1.0); + let direction = self.transform.rotation * forward; + + let uniform = LightUniform { + position: dvec3_to_uniform_array(self.transform.position), + direction: dvec3_direction_to_uniform_array(direction, self.light_component.outer_cutoff_angle), + colour: dvec3_colour_to_uniform_array( + self.light_component.colour * self.light_component.intensity as f64, + self.light_component.light_type, + ), + constant: self.light_component.attenuation.constant, + linear: self.light_component.attenuation.linear, + quadratic: self.light_component.attenuation.quadratic, + cutoff: f32::cos(self.light_component.cutoff_angle.to_radians()), + }; + + let cube_model = if let Some(lazy_model) = self.cube_lazy_model { + lazy_model.poke(graphics.clone())? + } else { + anyhow::bail!("The light cube LazyModel has not been initialised yet. Use Light::new(/** params */).preload_cube_model() to preload it (which is required)"); + }; + + let buffer = graphics.create_uniform(uniform, self.label.as_deref()); + + let layout = graphics.device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: self.label.as_deref(), + }); + + let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + label: self.label.as_deref(), + }); + + let instance = Instance::new( + self.transform.position, + self.transform.rotation, + DVec3::new(0.25, 0.25, 0.25), + ); + + let instance_buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: self.label.as_deref().or(Some("instance buffer")), + contents: bytemuck::cast_slice(&[instance.to_raw()]), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + + log::debug!("Created new light [{}]", label_str); + + Ok(Light { + uniform, + cube_model, + label: label_str, + buffer: Some(buffer), + layout: Some(layout), + bind_group: Some(bind_group), + instance_buffer: Some(instance_buffer), + }) + } +} + pub struct Light { pub uniform: LightUniform, cube_model: Model, @@ -208,8 +297,29 @@ pub struct Light { } impl Light { - pub fn new( - graphics: &Graphics, + pub async fn lazy_new( + light_component: LightComponent, + transform: Transform, + label: Option<&str>, + ) -> anyhow::Result<LazyLight> { + let mut result = LazyLight { + light_component: light_component, + transform: transform, + label: label.map(|s| s.to_string()), + cube_lazy_model: None, + }; + if result.cube_lazy_model.is_none() { + let lazy_model = Model::lazy_load( + include_bytes!("../../resources/cube.glb").to_vec(), + result.label.as_deref(), + ).await?; + result.cube_lazy_model = Some(lazy_model); + } + Ok(result) + } + + pub async fn new( + graphics: Arc<SharedGraphicsContext>, light: &LightComponent, transform: &Transform, label: Option<&str>, @@ -231,10 +341,11 @@ impl Light { }; let cube_model = Model::load_from_memory( - graphics, + graphics.clone(), include_bytes!("../../resources/cube.glb").to_vec(), label.clone(), ) + .await .unwrap(); let label_str = label.clone().unwrap_or("Light").to_string(); @@ -243,7 +354,6 @@ impl Light { let layout = graphics - .state .device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { entries: &[wgpu::BindGroupLayoutEntry { @@ -260,7 +370,6 @@ impl Light { }); let bind_group = graphics - .state .device .create_bind_group(&wgpu::BindGroupDescriptor { layout: &layout, @@ -279,7 +388,6 @@ impl Light { let instance_buffer = graphics - .state .device .create_buffer_init(&wgpu::util::BufferInitDescriptor { label: match label { @@ -363,10 +471,9 @@ impl LightManager { } } - pub fn create_light_array_resources(&mut self, graphics: &Graphics) { + pub fn create_light_array_resources(&mut self, graphics: Arc<SharedGraphicsContext>) { let layout = graphics - .state .device .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { entries: &[wgpu::BindGroupLayoutEntry { @@ -385,7 +492,6 @@ impl LightManager { let buffer = graphics.create_uniform(LightArrayUniform::default(), Some("Light Array")); let bind_group = graphics - .state .device .create_bind_group(&wgpu::BindGroupDescriptor { layout: &layout, @@ -401,7 +507,7 @@ impl LightManager { self.light_array_bind_group = Some(bind_group); } - pub fn update(&mut self, graphics: &Graphics, world: &hecs::World) { + pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>, world: &hecs::World) { let mut light_array = LightArrayUniform::default(); let mut light_index = 0; @@ -415,7 +521,7 @@ impl LightManager { if let Some(instance_buffer) = &light.instance_buffer { let instance_raw = instance.to_raw(); - graphics.state.queue.write_buffer( + graphics.queue.write_buffer( instance_buffer, 0, bytemuck::cast_slice(&[instance_raw]), @@ -432,7 +538,6 @@ impl LightManager { if let Some(buffer) = &self.light_array_buffer { graphics - .state .queue .write_buffer(buffer, 0, bytemuck::cast_slice(&[light_array])); } @@ -450,14 +555,14 @@ impl LightManager { pub fn create_render_pipeline( &mut self, - graphics: &mut Graphics, + graphics: Arc<SharedGraphicsContext>, shader_contents: &str, camera: &Camera, label: Option<&str>, ) { use crate::graphics::Shader; - let shader = Shader::new(graphics, shader_contents, label.clone()); + let shader = Shader::new(graphics.clone(), shader_contents, label.clone()); let pipeline = Self::create_render_pipeline_for_lighting( graphics, @@ -471,14 +576,13 @@ impl LightManager { } fn create_render_pipeline_for_lighting( - graphics: &mut Graphics, + graphics: Arc<SharedGraphicsContext>, shader: &Shader, bind_group_layouts: Vec<&BindGroupLayout>, label: Option<&str>, ) -> RenderPipeline { let render_pipeline_layout = graphics - .state .device .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some(label.unwrap_or("Light Render Pipeline Descriptor")), @@ -487,7 +591,6 @@ impl LightManager { }); graphics - .state .device .create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Render Pipeline"), @@ -1,4 +1,4 @@ -use crate::graphics::{Graphics, Texture}; +use crate::graphics::{SharedGraphicsContext, Texture}; use crate::utils::ResourceReference; use image::GenericImageView; use lazy_static::lazy_static; @@ -9,6 +9,7 @@ use parking_lot::Mutex; // scene::{PostProcess, Scene}, // }; use std::collections::HashMap; +use std::sync::Arc; use std::time::Instant; use std::{mem, ops::Range, path::PathBuf}; use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt}; @@ -45,12 +46,260 @@ pub struct Mesh { pub material: usize, } +#[derive(Default, Clone)] +pub struct ParsedModelData { + pub label: String, + pub path: ResourceReference, + pub mesh_data: Vec<ParsedMeshData>, + pub material_data: Vec<ParsedMaterialData>, +} + +#[derive(Default, Clone)] +pub struct ParsedMeshData { + pub name: String, + pub vertices: Vec<ModelVertex>, + pub indices: Vec<u32>, + pub material_index: usize, +} + +#[derive(Default, Clone)] +pub struct ParsedMaterialData { + pub name: String, + pub rgba_data: Vec<u8>, + pub dimensions: (u32, u32), +} + +pub trait LazyType { + type T; + fn poke(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<Self::T>; +} + +/// Loads the model into memory but graphics functions are defined after the creation +/// of the model +#[derive(Default)] +pub struct LazyModel { + parsed_data: ParsedModelData, +} + +impl LazyType for LazyModel { + type T = Model; + + fn poke(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<Self::T> { + let start = Instant::now(); + + let cache_key = self.parsed_data.label.clone(); + if let Some(cached_model) = MEMORY_MODEL_CACHE.lock().get(&cache_key) { + log::debug!("Model loaded from cache during poke: {:?}", cache_key); + return Ok(cached_model.clone()); + } + + log::debug!("Creating GPU resources for model: {:?}", self.parsed_data.label); + + let mut materials = Vec::new(); + for material_data in &self.parsed_data.material_data { + let texture_start = Instant::now(); + + let diffuse_texture = Texture::from_rgba_buffer( + graphics.clone(), + &material_data.rgba_data, + material_data.dimensions + ); + + let bind_group = diffuse_texture.bind_group().to_owned(); + + materials.push(Material { + name: material_data.name.clone(), + diffuse_texture, + bind_group, + }); + + log::debug!("Created GPU texture for material '{}' in {:?}", + material_data.name, texture_start.elapsed()); + } + + let mut meshes = Vec::new(); + for mesh_data in &self.parsed_data.mesh_data { + let buffer_start = Instant::now(); + + let vertex_buffer = graphics.clone() + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{} Vertex Buffer", mesh_data.name)), + contents: bytemuck::cast_slice(&mesh_data.vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + + let index_buffer = graphics.clone() + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{} Index Buffer", mesh_data.name)), + contents: bytemuck::cast_slice(&mesh_data.indices), + usage: wgpu::BufferUsages::INDEX, + }); + + meshes.push(Mesh { + name: mesh_data.name.clone(), + vertex_buffer, + index_buffer, + num_elements: mesh_data.indices.len() as u32, + material: mesh_data.material_index, + }); + + log::debug!("Created GPU buffers for mesh '{}' in {:?}", + mesh_data.name, buffer_start.elapsed()); + } + + let model = Model { + meshes, + materials, + label: self.parsed_data.label.clone(), + path: self.parsed_data.path.clone(), + }; + + MEMORY_MODEL_CACHE.lock().insert(cache_key, model.clone()); + log::debug!("Model GPU resource creation completed in {:?}", start.elapsed()); + + Ok(model) + } +} + + impl Model { - pub fn load_from_memory( - graphics: &Graphics<'_>, + + /// Creates a [`LazyModel`]. + pub async fn lazy_load( + buffer: impl AsRef<[u8]>, + label: Option<&str>, + ) -> anyhow::Result<LazyModel> { + let start = Instant::now(); + let label_str = label.unwrap_or("default"); + + log::debug!("Starting lazy load for model: {:?}", label_str); + + let res_ref = ResourceReference::from_bytes(buffer.as_ref()); + let (gltf, buffers, _images) = gltf::import_slice(buffer.as_ref())?; + + let mut texture_data = Vec::new(); + for material in gltf.materials() { + log::debug!("Processing material: {:?}", material.name()); + let material_name = material.name().unwrap_or("Unnamed Material").to_string(); + + let image_data = if let Some(pbr) = material.pbr_metallic_roughness().base_color_texture() { + let texture_info = pbr.texture(); + let image = texture_info.source(); + match image.source() { + gltf::image::Source::View { view, mime_type: _ } => { + let buffer_data = &buffers[view.buffer().index()]; + let start = view.offset(); + let end = start + view.length(); + buffer_data[start..end].to_vec() + } + gltf::image::Source::Uri { uri, mime_type: _ } => { + log::warn!("External URI textures not supported: {}", uri); + GREY_TEXTURE_BYTES.to_vec() + } + } + } else { + GREY_TEXTURE_BYTES.to_vec() + }; + + texture_data.push((material_name, image_data)); + } + + if texture_data.is_empty() { + texture_data.push(("Default".to_string(), GREY_TEXTURE_BYTES.to_vec())); + } + + let parallel_start = Instant::now(); + let processed_materials: Vec<_> = texture_data + .into_par_iter() + .map(|(material_name, image_data)| { + let material_start = Instant::now(); + + let diffuse_image = image::load_from_memory(&image_data) + .expect("Failed to load image from memory"); + let diffuse_rgba = diffuse_image.to_rgba8(); + let dimensions = diffuse_image.dimensions(); + + log::debug!("Processed material '{}' in {:?}", material_name, material_start.elapsed()); + + ParsedMaterialData { + name: material_name, + rgba_data: diffuse_rgba.into_raw(), + dimensions, + } + }) + .collect(); + + log::debug!("Parallel material processing took: {:?}", parallel_start.elapsed()); + + let mut mesh_data = Vec::new(); + for mesh in gltf.meshes() { + log::debug!("Processing mesh: {:?}", mesh.name()); + for primitive in mesh.primitives() { + let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()])); + + let positions: Vec<[f32; 3]> = reader + .read_positions() + .ok_or_else(|| anyhow::anyhow!("Mesh missing positions"))? + .collect(); + + let normals: Vec<[f32; 3]> = reader + .read_normals() + .map(|iter| iter.collect()) + .unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]); + + let tex_coords: Vec<[f32; 2]> = reader + .read_tex_coords(0) + .map(|iter| iter.into_f32().collect()) + .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]); + + let vertices: Vec<ModelVertex> = positions + .iter() + .zip(normals.iter()) + .zip(tex_coords.iter()) + .map(|((pos, norm), tex)| ModelVertex { + position: *pos, + normal: *norm, + tex_coords: *tex, + }) + .collect(); + + let indices: Vec<u32> = reader + .read_indices() + .ok_or_else(|| anyhow::anyhow!("Mesh missing indices"))? + .into_u32() + .collect(); + + let material_index = primitive.material().index().unwrap_or(0); + + mesh_data.push(ParsedMeshData { + name: mesh.name().unwrap_or("Unnamed Mesh").to_string(), + vertices, + indices, + material_index, + }); + } + } + + let parsed_data = ParsedModelData { + label: label_str.to_string(), + path: res_ref, + mesh_data, + material_data: processed_materials, + }; + + log::debug!("Lazy load completed for model: {:?} in {:?}", label_str, start.elapsed()); + + Ok(LazyModel { parsed_data }) + } + + pub async fn load_from_memory( + graphics: Arc<SharedGraphicsContext>, buffer: impl AsRef<[u8]>, label: Option<&str> ) -> anyhow::Result<Model> { + println!("========== Benchmarking speed of loading {:?} ==========", label); let start = Instant::now(); let cache_key = label.unwrap_or("default").to_string(); @@ -124,7 +373,7 @@ impl Model { for (material_name, rgba_data, dimensions) in processed_textures { let start = Instant::now(); - let diffuse_texture = Texture::create_texture_from_rgba_data(graphics, &rgba_data, dimensions); + let diffuse_texture = Texture::from_rgba_buffer(graphics.clone(), &rgba_data, dimensions); let bind_group = diffuse_texture.bind_group().to_owned(); materials.push(Material { @@ -174,7 +423,6 @@ impl Model { .collect(); let vertex_buffer = graphics - .state .device .create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(&format!("{:?} Vertex Buffer", label)), @@ -183,7 +431,6 @@ impl Model { }); let index_buffer = graphics - .state .device .create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(&format!("{:?} Index Buffer", label)), @@ -212,13 +459,15 @@ impl Model { }; MEMORY_MODEL_CACHE.lock().insert(cache_key, model.clone()); + println!("==================== DONE ===================="); log::debug!("Model cached from memory: {:?}", label); log::debug!("Took {:?} to load model: {:?}", start.elapsed(), label); + println!("=============================================="); Ok(model) } - pub fn load( - graphics: &Graphics<'_>, + pub async fn load( + graphics: Arc<SharedGraphicsContext>, path: &PathBuf, label: Option<&str>, ) -> anyhow::Result<Model> { @@ -233,7 +482,7 @@ impl Model { } let buffer = std::fs::read(path)?; - let model = Self::load_from_memory(graphics, buffer, label)?; + let model = Self::load_from_memory(graphics, buffer, label).await?; MODEL_CACHE.lock().insert(path_str, model.clone()); log::debug!("Model cached and loaded: {:?}", file_name); @@ -1,12 +1,13 @@ use winit::event_loop::ActiveEventLoop; -use crate::{graphics::Graphics, input}; +use crate::input; use std::{cell::RefCell, collections::HashMap, rc::Rc}; +#[async_trait::async_trait] pub trait Scene { - fn load(&mut self, graphics: &mut Graphics); - fn update(&mut self, dt: f32, graphics: &mut Graphics); - fn render(&mut self, graphics: &mut Graphics); + async fn load<'a>(&mut self, graphics: &mut crate::graphics::RenderContext<'a>); + async fn update<'a>(&mut self, dt: f32, graphics: &mut crate::graphics::RenderContext<'a>); + async fn render<'a>(&mut self, graphics: &mut crate::graphics::RenderContext<'a>); fn exit(&mut self, event_loop: &ActiveEventLoop); /// By far a mess of a trait however it works. /// @@ -70,10 +71,10 @@ impl Manager { .insert(scene_name.to_string(), input_name.to_string()); } - pub async fn update( + pub async fn update<'a>( &mut self, dt: f32, - graphics: &mut Graphics<'_>, + graphics: &mut crate::graphics::RenderContext<'a>, event_loop: &ActiveEventLoop, ) { // transition scene @@ -84,7 +85,7 @@ impl Manager { } } if let Some(scene) = self.scenes.get_mut(&next_scene_name) { - scene.borrow_mut().load(graphics); + scene.borrow_mut().load(graphics).await; } self.current_scene = Some(next_scene_name); } @@ -92,7 +93,7 @@ impl Manager { // update scene if let Some(scene_name) = &self.current_scene { if let Some(scene) = self.scenes.get_mut(scene_name) { - scene.borrow_mut().update(dt, graphics); + scene.borrow_mut().update(dt, graphics).await; let command = scene.borrow_mut().run_command(); match command { SceneCommand::SwitchScene(target) => { @@ -101,7 +102,7 @@ impl Manager { // reload the scene if let Some(scene) = self.scenes.get_mut(current) { scene.borrow_mut().exit(event_loop); - scene.borrow_mut().load(graphics); + scene.borrow_mut().load(graphics).await; log::debug!("Reloaded scene: {}", current); } } else { @@ -122,10 +123,10 @@ impl Manager { } } - pub async fn render(&mut self, graphics: &mut Graphics<'_>) { + pub async fn render<'a>(&mut self, graphics: &mut crate::graphics::RenderContext<'a>) { if let Some(scene_name) = &self.current_scene { if let Some(scene) = self.scenes.get_mut(scene_name) { - scene.borrow_mut().render(graphics); + scene.borrow_mut().render(graphics).await; } } } @@ -2,12 +2,105 @@ /// /// Inspiration taken from `https://github.com/4tkbytes/RedLight/blob/main/src/RedLight/Entities/Plane.cs`, /// my old game engine made in C sharp, where this is the plane "algorithm". +use std::sync::Arc; use crate::entity::AdoptedEntity; -use crate::graphics::{Graphics, Texture}; +use crate::graphics::{SharedGraphicsContext, Texture}; use crate::model::{Material, Mesh, Model, ModelVertex}; use crate::utils::{ResourceReference, ResourceReferenceType}; +use futures::executor::block_on; +use image::GenericImageView; use wgpu::{AddressMode, util::DeviceExt}; +/// Lazily creates a new Plane. This can only be accessed through the Default trait (which you shouldn't use), +/// or the [`PlaneBuilder::lazy_build()`] (also taken from [`PlaneBuilder::new()`]). +#[derive(Default)] +pub struct LazyPlaneBuilder { + rgba_data: Vec<u8>, + dimensions: (u32, u32), + width: f32, + height: f32, + tiles_x: u32, + tiles_z: u32, + label: Option<String>, +} + +impl LazyPlaneBuilder { + pub fn poke(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<AdoptedEntity> { + let mut vertices = Vec::new(); + let mut indices = Vec::new(); + + for z in 0..=1 { + for x in 0..=1 { + let position = [ + (x as f32 - 0.5) * self.width, + 0.0, + (z as f32 - 0.5) * self.height, + ]; + let normal = [0.0, 1.0, 0.0]; + let tex_coords = [ + x as f32 * self.tiles_x as f32, + z as f32 * self.tiles_z as f32, + ]; + vertices.push(ModelVertex { + position, + tex_coords, + normal, + }); + } + } + + indices.extend_from_slice(&[0, 2, 1, 1, 2, 3]); + + let vertex_buffer = + graphics + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{:?} Vertex Buffer", self.label.as_deref())), + contents: bytemuck::cast_slice(&vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + let index_buffer = + graphics + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{:?} Index Buffer", self.label.as_deref())), + contents: bytemuck::cast_slice(&indices), + usage: wgpu::BufferUsages::INDEX, + }); + + let mesh = Mesh { + name: "plane".to_string(), + vertex_buffer, + index_buffer, + num_elements: indices.len() as u32, + material: 0, + }; + + let diffuse_texture = Texture::new_with_sampler_with_rgba_buffer( + graphics.clone(), + &self.rgba_data, + self.dimensions, + AddressMode::Repeat + ); + let bind_group = diffuse_texture.bind_group().clone(); + let material = Material { + name: "plane_material".to_string(), + diffuse_texture, + bind_group, + }; + + let model = Model { + label: self.label.as_deref().unwrap_or("Plane").to_string(), + path: ResourceReference::from_reference(ResourceReferenceType::Plane), + meshes: vec![mesh], + materials: vec![material], + }; + + Ok(block_on(AdoptedEntity::adopt(graphics, model))) + } +} + +/// Creates a plane in the form of an AdoptedEntity. pub struct PlaneBuilder { width: f32, height: f32, @@ -37,9 +130,34 @@ impl PlaneBuilder { self } - pub fn build( + pub async fn lazy_build( + mut self, + texture_bytes: &[u8], + label: Option<&str>, + ) -> anyhow::Result<LazyPlaneBuilder> { + if self.tiles_x == 0 && self.tiles_z == 0 { + self.tiles_x = self.width as u32; + self.tiles_z = self.height as u32; + } + + let img = image::load_from_memory(texture_bytes)?; + let rgba = img.to_rgba8(); + let dimensions = img.dimensions(); + + Ok(LazyPlaneBuilder { + rgba_data: rgba.into_raw(), + dimensions, + width: self.width, + height: self.height, + tiles_x: self.tiles_x, + tiles_z: self.tiles_z, + label: label.map(|s| s.to_string()), + }) + } + + pub async fn build( mut self, - graphics: &Graphics, + graphics: Arc<SharedGraphicsContext>, texture_bytes: &[u8], label: Option<&str>, ) -> anyhow::Result<AdoptedEntity> { @@ -74,7 +192,6 @@ impl PlaneBuilder { let vertex_buffer = graphics - .state .device .create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(&format!("{:?} Vertex Buffer", label)), @@ -83,7 +200,6 @@ impl PlaneBuilder { }); let index_buffer = graphics - .state .device .create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(&format!("{:?} Index Buffer", label)), @@ -100,7 +216,7 @@ impl PlaneBuilder { }; let diffuse_texture = - Texture::new_with_sampler(graphics, texture_bytes, AddressMode::Repeat); + Texture::new_with_sampler(graphics.clone(), texture_bytes, AddressMode::Repeat); let bind_group = diffuse_texture.bind_group().clone(); let material = Material { name: "plane_material".to_string(), @@ -115,6 +231,6 @@ impl PlaneBuilder { materials: vec![material], }; - Ok(AdoptedEntity::adopt(graphics, model, label)) + Ok(AdoptedEntity::adopt(graphics, model).await) } } @@ -8,7 +8,7 @@ readme = "README.md" [dependencies] anyhow.workspace = true -# app_dirs2.workspace = true +app_dirs2.workspace = true bincode.workspace = true chrono.workspace = true dropbear-engine.workspace = true @@ -25,8 +25,16 @@ ron.workspace = true serde.workspace = true winit.workspace = true wasmer.workspace = true -rayon = "1.11" lazy_static.workspace = true +rayon.workspace = true +reqwest.workspace = true +flate2.workspace = true +tar.workspace = true +zip.workspace = true +tokio.workspace = true + +[dev-dependencies] +pollster = "*" [features] editor = [] @@ -4,4 +4,3 @@ pub mod logging; pub mod scripting; pub mod states; pub mod utils; -pub mod model_ext; @@ -1,249 +0,0 @@ -use std::{collections::HashMap, hash::{DefaultHasher, Hash, Hasher}, path::PathBuf, sync::Arc}; - -use dropbear_engine::{graphics::Graphics, model::Model}; -use parking_lot::Mutex; -use rayon::prelude::*; - - -lazy_static::lazy_static! { - pub static ref GLOBAL_MODEL_LOADER: MultiModelLoader = MultiModelLoader::new(); -} - -#[derive(Clone)] -pub enum ModelLoadingStatus { - NotLoaded, - Processing, - Loaded, - Failed(String), -} - -pub trait ModelType: Send + Sync { - fn load(&self, graphics: &Graphics) -> anyhow::Result<Model>; - fn get_id(&self) -> String; -} - -pub struct ModelHandle { - pub status: ModelLoadingStatus, - pub id: u64, -} - -pub struct MultiModelLoader { - models: Arc<Mutex<Vec<Box<dyn ModelType>>>>, - handles: Arc<Mutex<HashMap<u64, ModelHandle>>>, - loaded_models: Arc<Mutex<HashMap<u64, Model>>>, -} - -impl MultiModelLoader { - /// Creates a new instance of a multimodel loader - pub fn new() -> Self { - Self { - models: Arc::new(Mutex::new(Vec::new())), - handles: Arc::new(Mutex::new(HashMap::new())), - loaded_models: Arc::new(Mutex::new(HashMap::new())), - } - } - - /// Pushes a model into the global model loader queue - pub fn push(&self, model: Box<dyn ModelType>) -> ModelHandle { - let mut hasher = DefaultHasher::new(); - model.get_id().hash(&mut hasher); - let id = hasher.finish(); - - let handle = ModelHandle { - status: ModelLoadingStatus::NotLoaded, - id, - }; - - { - let mut models = self.models.lock(); - models.push(model); - } - - { - let mut handles = self.handles.lock(); - handles.insert(id, ModelHandle { - status: ModelLoadingStatus::NotLoaded, - id, - }); - } - - handle - } - - /// Processes all models in the model loader queue - pub fn process(&self, graphics: &Graphics) { - let models = { - let mut models_guard = self.models.lock(); - std::mem::take(&mut *models_guard) - }; - - if models.is_empty() { - return; - } - - { - let mut handles = self.handles.lock(); - for handle in handles.values_mut() { - handle.status = ModelLoadingStatus::Processing; - } - } - - let results: Vec<(u64, anyhow::Result<Model>)> = models - .into_par_iter() - .map(|model| { - let mut hasher = DefaultHasher::new(); - model.get_id().hash(&mut hasher); - let id = hasher.finish(); - - let result = model.load(graphics); - (id, result) - }) - .collect(); - - { - let mut handles = self.handles.lock(); - let mut loaded_models = self.loaded_models.lock(); - - for (id, result) in results { - if let Some(handle) = handles.get_mut(&id) { - match result { - Ok(model) => { - handle.status = ModelLoadingStatus::Loaded; - loaded_models.insert(id, model); // Store the loaded model - } - Err(error) => { - handle.status = ModelLoadingStatus::Failed(format!("{}", error)); - } - } - } - } - } - } - - /// Exchanges a model handle to a model. This function deleted the handle from memory if loaded and returns the model, - /// else returns an error based on its status (but still keep the model). - pub fn exchange(&self, handle: ModelHandle) -> anyhow::Result<Model> { - match handle.status { - ModelLoadingStatus::Loaded => { - let mut loaded_models = self.loaded_models.lock(); - if let Some(model) = loaded_models.remove(&handle.id) { - { - let mut handles = self.handles.lock(); - handles.remove(&handle.id); - } - Ok(model) - } else { - anyhow::bail!("Model with handle ID {} was marked as loaded but not found in storage", handle.id) - } - } - ModelLoadingStatus::Failed(error) => { - anyhow::bail!("Model loading failed: {}", error) - } - ModelLoadingStatus::Processing => { - anyhow::bail!("Model is still processing, cannot exchange yet") - } - ModelLoadingStatus::NotLoaded => { - anyhow::bail!("Model has not been processed yet") - } - } - } - - /// Alternative to [`crate::model_ext::MultiModelLoader::exchange`], allowing you to use a handle_id instead - /// of a [`crate::model_ext::ModelHandle`] - pub fn exchange_by_id(&self, handle_id: u64) -> anyhow::Result<Model> { - let status = { - let handles = self.handles.lock(); - handles.get(&handle_id) - .map(|h| h.status.clone()) - .ok_or_else(|| anyhow::anyhow!("Handle with ID {} not found", handle_id))? - }; - - match status { - ModelLoadingStatus::Loaded => { - let mut loaded_models = self.loaded_models.lock(); - if let Some(model) = loaded_models.remove(&handle_id) { - // Also remove the handle since it's been consumed - { - let mut handles = self.handles.lock(); - handles.remove(&handle_id); - } - Ok(model) - } else { - anyhow::bail!("Model with handle ID {} was marked as loaded but not found in storage", handle_id) - } - } - ModelLoadingStatus::Failed(error) => { - anyhow::bail!("Model loading failed: {}", error) - } - ModelLoadingStatus::Processing => { - anyhow::bail!("Model is still processing, cannot exchange yet") - } - ModelLoadingStatus::NotLoaded => { - anyhow::bail!("Model has not been processed yet") - } - } - } - - /// Fetches the status of the handle - pub fn get_status(&self, handle_id: u64) -> Option<ModelLoadingStatus> { - let handles = self.handles.lock(); - handles.get(&handle_id).map(|v| v.status.clone()) - } - - /// Clears completed handles - pub fn clear_completed(&self) { - let mut handles = self.handles.lock(); - handles.retain(|_, handle| { - matches!(handle.status, ModelLoadingStatus::NotLoaded | ModelLoadingStatus::Processing) - }); - } -} - -pub struct PendingModel { - pub path: Option<PathBuf>, - pub bytes: Option<Vec<u8>>, - pub label: String, - pub model_type: ModelLoadType, -} - -pub enum ModelLoadType { - File, - Memory, -} - -impl ModelType for PendingModel { - fn load(&self, graphics: &Graphics) -> anyhow::Result<Model> { - match self.model_type { - ModelLoadType::File => { - if let Some(path) = &self.path { - log::debug!("Loading model from file: {}", path.display()); - let _model = Model::load(graphics, path, Some(&self.label))?; - Ok(_model) - } else { - anyhow::bail!("Unable to find path in pending model: {}", self.label) - } - } - ModelLoadType::Memory => { - if let Some(bytes) = &self.bytes { - log::debug!("Loading model from memory: {} bytes", bytes.len()); - let _model = dropbear_engine::model::Model::load_from_memory( - graphics, - bytes.clone(), - Some(&self.label) - )?; - Ok(_model) - } else { - anyhow::bail!("Unable to get bytes in pending model: {}", self.label) - } - } - } - } - - fn get_id(&self) -> String { - format!("{}_{}", self.label, match self.model_type { - ModelLoadType::File => "file", - ModelLoadType::Memory => "memory", - }) - } -} - @@ -1,4 +1,5 @@ pub mod dropbear; +pub mod build; use crate::input::InputState; use crate::scripting::dropbear::DropbearAPI; @@ -0,0 +1,406 @@ +use std::path::PathBuf; +use std::process::Command; +use app_dirs2::{AppInfo, AppDataType, app_dir}; +use tokio::fs; +use tokio::io::AsyncWriteExt; + +const GLEAM_VERSION: &'static str = "1.12.0"; +const BUN_VERSION: &'static str = "1.2.22"; +const JAVY_VERSION: &'static str = "6.0.0"; + +pub const APP_INFO: AppInfo = AppInfo { + name: "Eucalyptus", + author: "4tkbytes", +}; + +/// Compiles a gleam project into WASM through a pipeline. +pub struct GleamScriptCompiler { + #[allow(dead_code)] + project_location: PathBuf, +} + +impl GleamScriptCompiler { + pub fn new(project_location: &PathBuf) -> Self { + GleamScriptCompiler { + project_location: project_location.clone(), + } + } + + pub async fn build(self) -> anyhow::Result<()> { + Self::ensure_dependencies().await?; + Ok(()) + } + + pub async fn ensure_dependencies() -> anyhow::Result<()> { + println!("Checking dependencies..."); + + let gleam_available = Self::check_tool_in_path("gleam").await; + if gleam_available { + println!("Gleam already exists in path"); + } + let bun_available = Self::check_tool_in_path("bun").await; + if bun_available { + println!("Bun already exists in path"); + } + let javy_available = Self::check_tool_in_path("javy").await; + if javy_available { + println!("Javy already exists in path"); + } + + if gleam_available && bun_available && javy_available { + println!("All dependencies found in PATH"); + return Ok(()); + } + + if !(cfg!(target_os = "windows") || cfg!(target_os = "linux") || cfg!(target_os = "macos")) { + anyhow::bail!("The operating system is not supported for building the Gleam project") + } + + let app_dir = app_dir(AppDataType::UserData, &APP_INFO, "") + .map_err(|e| anyhow::anyhow!("Failed to get app directory: {}", e))?; + + if !gleam_available { + Self::download_gleam(&app_dir).await?; + } + + if !bun_available { + Self::download_bun(&app_dir).await?; + } + + if !javy_available { + Self::download_javy(&app_dir).await?; + } + + Ok(()) + } + + async fn check_tool_in_path(tool: &str) -> bool { + let cmd = if cfg!(target_os = "windows") { + Command::new("where").arg(tool).output() + } else { + Command::new("which").arg(tool).output() + }; + + match cmd { + Ok(output) => output.status.success(), + Err(_) => false, + } + } + + pub async fn download_gleam(app_dir: &PathBuf) -> anyhow::Result<()> { + let gleam_dir = app_dir.join("dependencies").join("gleam").join(GLEAM_VERSION); + + if gleam_dir.exists() { + println!("Gleam v{} already cached at {}", GLEAM_VERSION, app_dir.display()); + return Ok(()); + } + + println!("Downloading Gleam v{}...", GLEAM_VERSION); + + let gleam_link = Self::get_gleam_download_url()?; + Self::download_and_extract(&gleam_link, &gleam_dir, "gleam").await?; + + println!("Gleam v{} downloaded successfully", GLEAM_VERSION); + Ok(()) + } + + pub async fn download_bun(app_dir: &PathBuf) -> anyhow::Result<()> { + let bun_dir = app_dir.join("dependencies").join("bun").join(BUN_VERSION); + + if bun_dir.exists() { + println!("Bun v{} already cached at {}", BUN_VERSION, app_dir.display()); + return Ok(()); + } + + println!("Downloading Bun v{}...", BUN_VERSION); + + let bun_link = Self::get_bun_download_url()?; + Self::download_and_extract(&bun_link, &bun_dir, "bun").await?; + + println!("Bun v{} downloaded successfully", BUN_VERSION); + Ok(()) + } + + pub async fn download_javy(app_dir: &PathBuf) -> anyhow::Result<()> { + let javy_dir = app_dir.join("dependencies").join("javy").join(JAVY_VERSION); + + if javy_dir.exists() { + println!("Javy v{} already cached at {}", JAVY_VERSION, app_dir.display()); + return Ok(()); + } + + println!("Downloading Javy v{}...", JAVY_VERSION); + + let javy_link = Self::get_javy_download_url()?; + Self::download_and_extract(&javy_link, &javy_dir, "javy").await?; + + println!("Javy v{} downloaded successfully", JAVY_VERSION); + Ok(()) + } + + async fn download_and_extract(url: &str, target_dir: &PathBuf, tool_name: &str) -> anyhow::Result<()> { + fs::create_dir_all(target_dir).await?; + + let response = reqwest::get(url).await?; + let bytes = response.bytes().await?; + + let temp_file = target_dir.join(format!("{}_download", tool_name)); + let mut file = fs::File::create(&temp_file).await?; + file.write_all(&bytes).await?; + file.sync_all().await?; + drop(file); + + if url.ends_with(".zip") { + Self::extract_zip(&temp_file, target_dir).await?; + } else if url.ends_with(".tar.gz") { + Self::extract_tar_gz(&temp_file, target_dir).await?; + } else if url.ends_with(".gz") { + Self::extract_gz(&temp_file, target_dir, tool_name).await?; + } + + fs::remove_file(&temp_file).await?; + Ok(()) + } + + async fn extract_zip(archive: &PathBuf, target_dir: &PathBuf) -> anyhow::Result<()> { + let file = std::fs::File::open(archive)?; + let mut archive = zip::ZipArchive::new(file)?; + + for i in 0..archive.len() { + let mut file = archive.by_index(i)?; + let outpath = target_dir.join(file.name()); + + if file.is_dir() { + fs::create_dir_all(&outpath).await?; + } else { + if let Some(p) = outpath.parent() { + fs::create_dir_all(p).await?; + } + let mut outfile = fs::File::create(&outpath).await?; + let mut buffer = Vec::new(); + std::io::Read::read_to_end(&mut file, &mut buffer)?; + outfile.write_all(&buffer).await?; + } + } + Ok(()) + } + + async fn extract_tar_gz(archive: &PathBuf, target_dir: &PathBuf) -> anyhow::Result<()> { + let file = std::fs::File::open(archive)?; + let tar = flate2::read::GzDecoder::new(file); + let mut archive = tar::Archive::new(tar); + + for entry in archive.entries()? { + let mut entry = entry?; + let path = target_dir.join(entry.path()?); + entry.unpack(path)?; + } + Ok(()) + } + + async fn extract_gz(archive: &PathBuf, target_dir: &PathBuf, tool_name: &str) -> anyhow::Result<()> { + let file = std::fs::File::open(archive)?; + let mut decoder = flate2::read::GzDecoder::new(file); + let mut buffer = Vec::new(); + std::io::Read::read_to_end(&mut decoder, &mut buffer)?; + + let exe_name = if cfg!(target_os = "windows") { + format!("{}.exe", tool_name) + } else { + tool_name.to_string() + }; + + let output_path = target_dir.join(exe_name); + let mut output_file = fs::File::create(&output_path).await?; + output_file.write_all(&buffer).await?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = output_file.metadata().await?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&output_path, perms).await?; + } + + Ok(()) + } + + fn get_gleam_download_url() -> anyhow::Result<String> { + let gleam_link = { + #[cfg(target_os = "windows")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + anyhow::bail!("This architecture is not supported for building the gleam project"); + } + }; + format!("https://github.com/gleam-lang/gleam/releases/download/v{}/gleam-v{}-{}-pc-windows-msvc.zip", + GLEAM_VERSION, + GLEAM_VERSION, + arch, + ) + } + + #[cfg(target_os = "linux")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + anyhow::bail!("This architecture is not supported for building the gleam project"); + } + }; + format!("https://github.com/gleam-lang/gleam/releases/download/v{}/gleam-v{}-{}-unknown-linux-musl.tar.gz", + GLEAM_VERSION, + GLEAM_VERSION, + arch, + ) + } + + #[cfg(target_os = "macos")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + anyhow::bail!("This architecture is not supported for building the gleam project"); + } + }; + format!("https://github.com/gleam-lang/gleam/releases/download/v{}/gleam-v{}-{}-apple-darwin.tar.gz", + GLEAM_VERSION, + GLEAM_VERSION, + arch, + ) + } + }; + Ok(gleam_link) + } + + fn get_bun_download_url() -> anyhow::Result<String> { + let bun_link = { + #[cfg(target_os = "windows")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "x86_64") { + "x64" + } else { + anyhow::bail!("This architecture is not supported for building the gleam project"); + } + }; + format!("https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-windows-{}.zip", + BUN_VERSION, + arch, + ) + } + + #[cfg(target_os = "linux")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "x86_64") { + "x64" + } else { + anyhow::bail!("This architecture is not supported for building the gleam project"); + } + }; + format!("https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-linux-{}.zip", + BUN_VERSION, + arch, + ) + } + + #[cfg(target_os = "macos")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "aarch64" + } else if cfg!(target_arch = "x86_64") { + "x64" + } else { + anyhow::bail!("This architecture is not supported for building the gleam project"); + } + }; + format!("https://github.com/oven-sh/bun/releases/download/bun-v{}/bun-darwin-{}.zip", + BUN_VERSION, + arch, + ) + } + }; + Ok(bun_link) + } + + fn get_javy_download_url() -> anyhow::Result<String> { + let javy_link = { + #[cfg(target_os = "windows")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + anyhow::bail!("This arch is not available for prebuilt download. Please build this from source"); + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + anyhow::bail!("This architecture is not supported for building the gleam project"); + } + }; + format!("https://github.com/bytecodealliance/javy/releases/download/v{}/javy-{}-windows-v{}.gz", + JAVY_VERSION, + arch, + JAVY_VERSION + ) + } + + #[cfg(target_os = "linux")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "arm" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + anyhow::bail!("This architecture is not supported for building the gleam project"); + } + }; + format!("https://github.com/bytecodealliance/javy/releases/download/v{}/javy-{}-linux-v{}.gz", + JAVY_VERSION, + arch, + JAVY_VERSION + ) + } + + #[cfg(target_os = "macos")] + { + let arch = { + if cfg!(target_arch = "aarch64") { + "arm" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + anyhow::bail!("This architecture is not supported for building the gleam project"); + } + }; + format!("https://github.com/bytecodealliance/javy/releases/download/v{}/javy-{}-macos-v{}.gz", + JAVY_VERSION, + arch, + JAVY_VERSION + ) + } + }; + Ok(javy_link) + } +} + +#[tokio::test] +async fn check_if_dependencies_install() { + GleamScriptCompiler::ensure_dependencies().await.unwrap(); +} @@ -1,12 +1,12 @@ use crate::camera::DebugCamera; use crate::camera::{CameraComponent, CameraFollowTarget, CameraType}; -use crate::model_ext::PendingModel; use crate::utils::PROTO_TEXTURE; use chrono::Utc; use dropbear_engine::camera::Camera; use dropbear_engine::entity::{AdoptedEntity, Transform}; -use dropbear_engine::graphics::Graphics; +use dropbear_engine::graphics::SharedGraphicsContext; use dropbear_engine::lighting::{Light, LightComponent}; +use dropbear_engine::model::Model; use dropbear_engine::starter::plane::PlaneBuilder; use dropbear_engine::utils::{ResourceReference, ResourceReferenceType}; use egui_dock_fork::DockState; @@ -18,6 +18,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::path::PathBuf; +use std::sync::Arc; use std::{fmt, fs}; pub static PROJECT: Lazy<RwLock<ProjectConfig>> = @@ -713,10 +714,10 @@ impl SceneConfig { Ok(config) } - pub fn load_into_world( + pub async fn load_into_world( &self, world: &mut hecs::World, - graphics: &Graphics, + graphics: Arc<SharedGraphicsContext>, ) -> anyhow::Result<hecs::Entity> { log::info!( "Loading scene [{}], clearing world with {} entities", @@ -736,12 +737,10 @@ impl SceneConfig { log::info!("World cleared, now has {} entities", world.len()); - let mut model_handles = Vec::new(); - let mut pending_entities = Vec::new(); - - for entity_config in &self.entities { + for (_entity_index, entity_config) in self.entities.iter().enumerate() { log::debug!("Loading entity: {}", entity_config.label); - match &entity_config.model_path.ref_type { + + let result = match &entity_config.model_path.ref_type { ResourceReferenceType::File(reference) => { let path: PathBuf = { if cfg!(feature = "editor") { @@ -760,7 +759,6 @@ impl SceneConfig { entity_config.model_path.to_executable_path()? } }; - log::debug!( "Path for entity {} is {} from reference {}", entity_config.label, @@ -768,33 +766,96 @@ impl SceneConfig { reference ); - let pending_model = PendingModel { - path: Some(path), - bytes: None, - label: entity_config.label.clone(), - model_type: crate::model_ext::ModelLoadType::File, - }; + let adopted = AdoptedEntity::new(graphics.clone(), &path, Some(&entity_config.label)).await; + + // let adopted = { + // let path_clone = path.clone(); + // let label_clone = entity_config.label.clone(); + + // let (tx, mut rx) = tokio::sync::oneshot::channel(); + + // tokio::task::spawn_local(async move { + // let entity = LazyAdoptedEntity::from_file(&path_clone, Some(&label_clone)).await; + // let _ = tx.send(entity); + // }); + + // loop { + // match rx.try_recv() { + // Ok(result) => { + // break result?.poke(graphics)?; + // }, + // Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { + // std::thread::yield_now(); + // continue; + // } + // Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { + // return Err(anyhow::anyhow!("Loading task was cancelled")); + // } + // } + // } + // }; + let transform = entity_config.transform; - let handle = crate::model_ext::GLOBAL_MODEL_LOADER.push(Box::new(pending_model)); - model_handles.push((handle.id, entity_config.clone())); - pending_entities.push(entity_config.clone()); + if let Some(script_config) = &entity_config.script { + let script = ScriptComponent { + name: script_config.name.clone(), + path: script_config.path.clone(), + }; + world.spawn((adopted, transform, script, entity_config.properties.clone())); + } else { + world.spawn((adopted, transform, entity_config.properties.clone())); + } + + Ok(()) } ResourceReferenceType::Bytes(bytes) => { - log::info!("Queuing entity from bytes [Len: {}]", bytes.len()); - - let pending_model = PendingModel { - path: None, - bytes: Some(bytes.to_owned()), - label: entity_config.label.clone(), - model_type: crate::model_ext::ModelLoadType::Memory, - }; + log::info!("Loading entity from bytes [Len: {}]", bytes.len()); + let bytes = bytes.to_owned(); + + let model = Model::load_from_memory(graphics.clone(), bytes, Some(&entity_config.label)).await?; + let adopted = AdoptedEntity::adopt(graphics.clone(), model).await; + + // let adopted = { + // let bytes_clone = bytes.clone(); + // let label_clone = entity_config.label.clone(); + + // let (tx, mut rx) = tokio::sync::oneshot::channel(); + + // tokio::task::spawn_local(async move { + // let entity = LazyAdoptedEntity::from_memory(bytes_clone, Some(label_clone.as_str())).await; + // let _ = tx.send(entity); + // }); + + // loop { + // match rx.try_recv() { + // Ok(result) => { + // break result?.poke(graphics)?; + // }, + // Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { + // std::thread::yield_now(); + // continue; + // } + // Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { + // return Err(anyhow::anyhow!("Loading task was cancelled")); + // } + // } + // } + // }; + let transform = entity_config.transform; - let handle = crate::model_ext::GLOBAL_MODEL_LOADER.push(Box::new(pending_model)); - model_handles.push((handle.id, entity_config.clone())); - pending_entities.push(entity_config.clone()); + if let Some(script_config) = &entity_config.script { + let script = ScriptComponent { + name: script_config.name.clone(), + path: script_config.path.clone(), + }; + world.spawn((adopted, transform, script, entity_config.properties.clone())); + } else { + world.spawn((adopted, transform, entity_config.properties.clone())); + } + + Ok(()) } ResourceReferenceType::Plane => { - // this can be loaded in immediately as it doesn't use IO to load let width = entity_config .properties .custom_properties @@ -832,14 +893,47 @@ impl SceneConfig { _ => panic!("Entity has a tiles_z property that is not an int"), }; + let label_clone = entity_config.label.clone(); + let width_val = *width as f32; + let height_val = *height as f32; + let tiles_x_val = *tiles_x as u32; + let tiles_z_val = *tiles_z as u32; + let plane = PlaneBuilder::new() - .with_size(*width as f32, *height as f32) - .with_tiles(*tiles_x as u32, *tiles_z as u32) - .build( - graphics, - PROTO_TEXTURE, - Some(entity_config.label.clone().as_str()), - )?; + .with_size(width_val, height_val) + .with_tiles(tiles_x_val, tiles_z_val) + .build(graphics.clone(), PROTO_TEXTURE, Some(&label_clone)).await?; + + // let plane = { + // let label_clone = entity_config.label.clone(); + // let width_val = *width as f32; + // let height_val = *height as f32; + // let tiles_x_val = *tiles_x as u32; + // let tiles_z_val = *tiles_z as u32; + + // let (tx, mut rx) = tokio::sync::oneshot::channel(); + + // tokio::task::spawn_local(async move { + // let result = PlaneBuilder::new() + // .with_size(width_val, height_val) + // .with_tiles(tiles_x_val, tiles_z_val) + // .lazy_build(PROTO_TEXTURE, Some(&label_clone)).await; + // let _ = tx.send(result); + // }); + + // loop { + // match rx.try_recv() { + // Ok(result) => break result?.poke(graphics)?, + // Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { + // std::thread::yield_now(); + // continue; + // } + // Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { + // return Err(anyhow::anyhow!("Loading task was cancelled")); + // } + // } + // } + // }; let transform = entity_config.transform; if let Some(script_config) = &entity_config.script { @@ -851,48 +945,54 @@ impl SceneConfig { } else { world.spawn((plane, transform, entity_config.properties.clone())); } - } - ResourceReferenceType::None => panic!( - "Entity has a resource reference of None, which cannot be loaded or referenced" - ), - } - } - log::info!("Processing {} models in parallel", model_handles.len()); - crate::model_ext::GLOBAL_MODEL_LOADER.process(graphics); - - for (handle_id, entity_config) in model_handles { - match crate::model_ext::GLOBAL_MODEL_LOADER.get_status(handle_id) { - Some(crate::model_ext::ModelLoadingStatus::Loaded) => { - log::debug!("Model loaded successfully: {}", entity_config.label); - - let model = crate::model_ext::GLOBAL_MODEL_LOADER.exchange_by_id(handle_id)?; - let adopted = AdoptedEntity::adopt(graphics, model, Some(&entity_config.label)); - - self.spawn_entity_with_components(world, &entity_config, adopted); - } - Some(crate::model_ext::ModelLoadingStatus::Failed(error)) => { - log::error!("Failed to load model {}: {}", entity_config.label, error); - return Err(anyhow::anyhow!("Failed to load model {}: {}", entity_config.label, error)); + Ok(()) } - _ => { - log::error!("Model loading status unknown for: {}", entity_config.label); - return Err(anyhow::anyhow!("Model loading failed for: {}", entity_config.label)); + ResourceReferenceType::None => { + Err(anyhow::anyhow!( + "Entity has a resource reference of None, which cannot be loaded or referenced" + )) } - } - } + }; - crate::model_ext::GLOBAL_MODEL_LOADER.clear_completed(); + result?; + // processed_items += 1; + log::debug!("Loaded!"); + } for light_config in &self.lights { log::debug!("Loading light: {}", light_config.label); - let light = Light::new( - graphics, - &light_config.light_component, - &light_config.transform, - Some(&light_config.label), - ); + let light = Light::new(graphics.clone(), &light_config.light_component, &light_config.transform, Some(&light_config.label)).await; + + // let light = { + // let light_comp_clone = light_config.light_component.clone(); + // let light_trans_clone = light_config.transform.clone(); + // let label_clone = light_config.label.clone(); + // let (tx, mut rx) = tokio::sync::oneshot::channel(); + + // tokio::task::spawn_local(async move { + // let result = Light::lazy_new( + // light_comp_clone, + // light_trans_clone, + // Some(&label_clone), + // ); + // let _ = tx.send(result); + // }); + + // loop { + // match rx.try_recv() { + // Ok(result) => break result.poke(graphics)?, + // Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { + // std::thread::yield_now(); + // continue; + // } + // Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { + // return Err(anyhow::anyhow!("Loading task was cancelled")); + // } + // } + // } + // }; world.spawn(( light_config.light_component.clone(), @@ -900,6 +1000,8 @@ impl SceneConfig { light, ModelProperties::default(), )); + + // processed_items += 1; } for camera_config in &self.cameras { @@ -910,7 +1012,7 @@ impl SceneConfig { ); let camera = Camera::new( - graphics, + graphics.clone(), DVec3::from_array(camera_config.position), DVec3::from_array(camera_config.target), DVec3::from_array(camera_config.up), @@ -942,6 +1044,8 @@ impl SceneConfig { } else { world.spawn((camera, component)); } + + // processed_items += 1; } if world @@ -951,21 +1055,47 @@ impl SceneConfig { .is_none() { log::info!("No lights in scene, spawning default light"); - let default_transform = Transform { - position: glam::DVec3::new(2.0, 4.0, 2.0), - ..Default::default() - }; - let default_component = LightComponent::directional(glam::DVec3::ONE, 1.0); - let default_light = Light::new( - graphics, - &default_component, - &default_transform, - Some("Default Light"), - ); + let comp = LightComponent::directional(glam::DVec3::ONE, 1.0); + let trans = Transform { + position: glam::DVec3::new(2.0, 4.0, 2.0), + ..Default::default() + }; + let light = Light::new(graphics.clone(), &comp, &trans, Some("Default Light")).await; + // let light = { + // let light_comp_clone = LightComponent::directional(glam::DVec3::ONE, 1.0); + // let default_transform = Transform { + // position: glam::DVec3::new(2.0, 4.0, 2.0), + // ..Default::default() + // }; + // let (tx, mut rx) = tokio::sync::oneshot::channel(); + + // tokio::task::spawn_local(async move { + // let result = Light::lazy_new( + // light_comp_clone, + // default_transform, + // Some("Default Light"), + // ); + // let _ = tx.send(result); + // }); + + // loop { + // match rx.try_recv() { + // Ok(result) => break result.poke(graphics)?, + // Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { + // std::thread::yield_now(); + // continue; + // } + // Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { + // return Err(anyhow::anyhow!("Loading task was cancelled")); + // } + // } + // } + // }; + world.spawn(( - default_component, - default_transform, - default_light, + comp, + trans, + light, ModelProperties::default(), )); } @@ -995,7 +1125,7 @@ impl SceneConfig { Ok(camera_entity) } else { log::info!("No debug camera found, creating viewport camera for editor"); - let camera = Camera::predetermined(graphics, Some("Viewport Camera")); + let camera = Camera::predetermined(graphics.clone(), Some("Viewport Camera")); let component = DebugCamera::new(); let camera_entity = world.spawn((camera, component)); Ok(camera_entity) @@ -1024,25 +1154,6 @@ impl SceneConfig { } } } - - fn spawn_entity_with_components( - &self, - world: &mut hecs::World, - entity_config: &SceneEntity, - adopted: AdoptedEntity, - ) { - let transform = entity_config.transform; - - if let Some(script_config) = &entity_config.script { - let script = ScriptComponent { - name: script_config.name.clone(), - path: script_config.path.clone(), - }; - world.spawn((adopted, transform, script, entity_config.properties.clone())); - } else { - world.spawn((adopted, transform, entity_config.properties.clone())); - } - } } #[derive(bincode::Decode, bincode::Encode, serde::Serialize, serde::Deserialize, Debug)] @@ -34,6 +34,8 @@ winit.workspace = true clap.workspace = true walkdir.workspace = true zip.workspace = true +tokio.workspace = true +async-trait.workspace = true [target.'cfg(not(target_os = "android"))'.dependencies] rfd.workspace = true @@ -1,9 +1,40 @@ //! Used to aid with debugging any issues with the editor. use crate::editor::Signal; -use egui::Ui; +use egui::{Ui, Window, ProgressBar}; +use eucalyptus_core::scripting::build::GleamScriptCompiler; +use tokio::sync::mpsc; + +#[derive(Debug, Clone)] +pub enum DependencyProgress { + Starting, + CheckingPath(String), + Downloading(String), + Extracting(String), + Completed(String), + Error(String), + Finished, +} + +pub struct DependencyInstaller { + pub progress_receiver: Option<mpsc::UnboundedReceiver<DependencyProgress>>, + pub current_progress: Vec<String>, + pub is_installing: bool, + pub progress_value: f32, +} + +impl Default for DependencyInstaller { + fn default() -> Self { + Self { + progress_receiver: None, + current_progress: Vec::new(), + is_installing: false, + progress_value: 0.0, + } + } +} /// Show a menu bar for debug. A new "Debug" menu button will show up on the editors menu bar. -pub(crate) fn show_menu_bar(ui: &mut Ui, signal: &mut Signal) { +pub(crate) fn show_menu_bar(ui: &mut Ui, signal: &mut Signal, dependency_installer: &mut DependencyInstaller) { ui.menu_button("Debug", |ui_debug| { if ui_debug.button("Panic").clicked() { log::warn!("Panic caused on purpose from Menu Button Click"); @@ -14,5 +45,205 @@ pub(crate) fn show_menu_bar(ui: &mut Ui, signal: &mut Signal) { log::info!("Show Entities Loaded under Debug Menu is clicked"); *signal = Signal::LogEntities; } + + ui_debug.add_enabled_ui(!dependency_installer.is_installing, |ui| { + if ui.button("Ensure dependencies").clicked() { + log::info!("Clicked ensure dependencies from debug menu"); + start_dependency_installation(dependency_installer); + } + }); + + if ui_debug.button("Evaluate script").clicked() { + log::info!("Evaluating script"); + } + }); +} + +pub(crate) fn show_dependency_progress_window( + ctx: &egui::Context, + dependency_installer: &mut DependencyInstaller, +) { + if let Some(receiver) = &mut dependency_installer.progress_receiver { + while let Ok(progress) = receiver.try_recv() { + match &progress { + DependencyProgress::Starting => { + dependency_installer.current_progress.clear(); + dependency_installer.current_progress.push("Starting dependency check...".to_string()); + dependency_installer.progress_value = 0.0; + } + DependencyProgress::CheckingPath(tool) => { + dependency_installer.current_progress.push(format!("Checking if {} is in PATH...", tool)); + dependency_installer.progress_value = 0.1; + } + DependencyProgress::Downloading(tool) => { + dependency_installer.current_progress.push(format!("Downloading {}...", tool)); + dependency_installer.progress_value += 0.25; + } + DependencyProgress::Extracting(tool) => { + dependency_installer.current_progress.push(format!("Extracting {}...", tool)); + dependency_installer.progress_value += 0.1; + } + DependencyProgress::Completed(tool) => { + dependency_installer.current_progress.push(format!("✓ {} ready", tool)); + dependency_installer.progress_value += 0.1; + } + DependencyProgress::Error(error) => { + dependency_installer.current_progress.push(format!("❌ Error: {}", error)); + } + DependencyProgress::Finished => { + dependency_installer.current_progress.push("✓ All dependencies ready!".to_string()); + dependency_installer.progress_value = 1.0; + dependency_installer.is_installing = false; + // dependency_installer.progress_receiver = None; + } + } + } + } + + if dependency_installer.is_installing || !dependency_installer.current_progress.is_empty() { + Window::new("Dependency Installation") + .collapsible(false) + .resizable(false) + .show(ctx, |ui| { + ui.vertical(|ui| { + ui.add( + ProgressBar::new(dependency_installer.progress_value) + .show_percentage() + .animate(dependency_installer.is_installing) + ); + + ui.separator(); + + egui::ScrollArea::vertical() + .max_height(200.0) + .show(ui, |ui| { + for message in &dependency_installer.current_progress { + ui.label(message); + } + }); + + if !dependency_installer.is_installing && ui.button("Close").clicked() { + dependency_installer.current_progress.clear(); + } + }); + }); + } +} + +fn start_dependency_installation(dependency_installer: &mut DependencyInstaller) { + let (sender, receiver) = mpsc::unbounded_channel(); + dependency_installer.progress_receiver = Some(receiver); + dependency_installer.is_installing = true; + dependency_installer.progress_value = 0.0; + + tokio::spawn(async move { + let _ = sender.send(DependencyProgress::Starting); + + match ensure_dependencies_with_progress(sender.clone()).await { + Ok(_) => { + let _ = sender.send(DependencyProgress::Finished); + } + Err(e) => { + let _ = sender.send(DependencyProgress::Error(e.to_string())); + let _ = sender.send(DependencyProgress::Finished); + } + } }); } + +async fn ensure_dependencies_with_progress( + progress_sender: mpsc::UnboundedSender<DependencyProgress>, +) -> anyhow::Result<()> { + let tools = vec![ + ("gleam", "Gleam"), + ("bun", "Bun"), + ("javy", "Javy"), + ]; + + let mut tools_to_download = Vec::new(); + + for (tool_cmd, tool_name) in &tools { + let _ = progress_sender.send(DependencyProgress::CheckingPath(tool_name.to_string())); + + let available = check_tool_in_path(tool_cmd).await; + if !available { + tools_to_download.push((*tool_cmd, *tool_name)); + } else { + let _ = progress_sender.send(DependencyProgress::Completed(format!("{} (found in PATH)", tool_name))); + } + } + + if tools_to_download.is_empty() { + return Ok(()); + } + + let app_dir = app_dirs2::app_dir(app_dirs2::AppDataType::UserData, &eucalyptus_core::scripting::build::APP_INFO, "") + .map_err(|e| anyhow::anyhow!("Failed to get app directory: {}", e))?; + + for (tool_cmd, tool_name) in tools_to_download { + let _ = progress_sender.send(DependencyProgress::Downloading(tool_name.to_string())); + + match tool_cmd { + "gleam" => { + if let Err(e) = download_gleam_with_progress(&app_dir, progress_sender.clone()).await { + let _ = progress_sender.send(DependencyProgress::Error(format!("Failed to download Gleam: {}", e))); + return Err(e); + } + } + "bun" => { + if let Err(e) = download_bun_with_progress(&app_dir, progress_sender.clone()).await { + let _ = progress_sender.send(DependencyProgress::Error(format!("Failed to download Bun: {}", e))); + return Err(e); + } + } + "javy" => { + if let Err(e) = download_javy_with_progress(&app_dir, progress_sender.clone()).await { + let _ = progress_sender.send(DependencyProgress::Error(format!("Failed to download Javy: {}", e))); + return Err(e); + } + } + _ => {} + } + + let _ = progress_sender.send(DependencyProgress::Completed(tool_name.to_string())); + } + + Ok(()) +} + +async fn check_tool_in_path(tool: &str) -> bool { + let cmd = if cfg!(target_os = "windows") { + std::process::Command::new("where").arg(tool).output() + } else { + std::process::Command::new("which").arg(tool).output() + }; + + match cmd { + Ok(output) => output.status.success(), + Err(_) => false, + } +} + +async fn download_gleam_with_progress( + app_dir: &std::path::PathBuf, + progress_sender: mpsc::UnboundedSender<DependencyProgress>, +) -> anyhow::Result<()> { + let _ = progress_sender.send(DependencyProgress::Extracting("Gleam".to_string())); + GleamScriptCompiler::download_gleam(app_dir).await +} + +async fn download_bun_with_progress( + app_dir: &std::path::PathBuf, + progress_sender: mpsc::UnboundedSender<DependencyProgress>, +) -> anyhow::Result<()> { + let _ = progress_sender.send(DependencyProgress::Extracting("Bun".to_string())); + GleamScriptCompiler::download_bun(app_dir).await +} + +async fn download_javy_with_progress( + app_dir: &std::path::PathBuf, + progress_sender: mpsc::UnboundedSender<DependencyProgress>, +) -> anyhow::Result<()> { + let _ = progress_sender.send(DependencyProgress::Extracting("Javy".to_string())); + GleamScriptCompiler::download_javy(app_dir).await +} @@ -37,32 +37,39 @@ pub struct EditorTabViewer<'a> { } impl<'a> EditorTabViewer<'a> { - fn spawn_entity_at_pos(&mut self, asset: &DraggedAsset, position: DVec3, properties: Option<ModelProperties>) -> anyhow::Result<()> { - let pending_model = eucalyptus_core::model_ext::PendingModel { - path: Some(asset.path.clone()), - bytes: None, - label: asset.name.clone(), - model_type: eucalyptus_core::model_ext::ModelLoadType::File, - }; - - let handle = eucalyptus_core::model_ext::GLOBAL_MODEL_LOADER.push(Box::new(pending_model)); - + fn spawn_entity_at_pos( + &mut self, + asset: &DraggedAsset, + position: DVec3, + properties: Option<ModelProperties>, + ) -> anyhow::Result<()> { let mut transform = Transform::default(); transform.position = position; - - let mut pending_spawns = PENDING_SPAWNS.lock(); - pending_spawns.push(PendingSpawn { - asset_path: asset.path.clone(), - asset_name: asset.name.clone(), - transform, - properties: properties.unwrap_or_default(), - handle_id: Some(handle.id), - }); - - Ok(()) + { + let mut pending_spawns = PENDING_SPAWNS.lock(); + if let Some(props) = properties { + pending_spawns.push(PendingSpawn { + asset_path: asset.path.clone(), + asset_name: asset.name.clone(), + transform, + properties: props, + handle_id: None, + }); + } else { + pending_spawns.push(PendingSpawn { + asset_path: asset.path.clone(), + asset_name: asset.name.clone(), + transform, + properties: ModelProperties::default(), + handle_id: None, + }); + } + Ok(()) + } } } + #[derive(Clone, Debug)] pub struct DraggedAsset { pub name: String, @@ -12,21 +12,21 @@ use std::{ time::{Duration, Instant}, }; -use crate::build::build; +use crate::{build::build, debug::DependencyInstaller}; use crate::camera::UndoableCameraAction; use crate::debug; use dropbear_engine::{ camera::Camera, entity::{AdoptedEntity, Transform}, - graphics::Graphics, + graphics::SharedGraphicsContext, lighting::{Light, LightManager}, scene::SceneCommand, }; use egui::{self, Context}; use egui_dock_fork::{DockArea, DockState, NodeIndex, Style}; -use eucalyptus_core::camera::{ +use eucalyptus_core::{camera::{ CameraAction, CameraComponent, CameraFollowTarget, CameraType, DebugCamera, -}; +}}; use eucalyptus_core::input::InputState; use eucalyptus_core::scripting::{ScriptAction, ScriptManager}; use eucalyptus_core::states::{ @@ -78,6 +78,9 @@ pub struct Editor { play_mode_backup: Option<PlayModeBackup>, input_state: InputState, + + // channels + dep_installer: DependencyInstaller, } impl Editor { @@ -140,6 +143,7 @@ impl Editor { input_state: InputState::new(), light_manager: LightManager::new(), active_camera: None, + dep_installer: DependencyInstaller::default() // ..Default::default() // note to self: DO NOT USE ..DEFAULT::DEFAULT(), IT WILL CAUSE OVERFLOW } @@ -265,19 +269,26 @@ impl Editor { Ok(()) } - pub fn load_project_config(&mut self, graphics: &Graphics) -> anyhow::Result<()> { - let config = PROJECT.read(); + pub async fn load_project_config(&mut self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()> { + { + let config = PROJECT.read(); - self.project_path = Some(config.project_path.clone()); + self.project_path = Some(config.project_path.clone()); - if let Some(layout) = &config.dock_layout { - self.dock_state = layout.clone(); + if let Some(layout) = &config.dock_layout { + self.dock_state = layout.clone(); + } } - { + let first_scene_opt = { let scenes = SCENES.read(); - if let Some(first_scene) = scenes.first() { - self.active_camera = Some(first_scene.load_into_world(Arc::get_mut(&mut self.world).unwrap(), graphics)?); + scenes.first().cloned() + }; + + { + if let Some(first_scene) = first_scene_opt { + let cam = first_scene.load_into_world(Arc::get_mut(&mut self.world).unwrap(), graphics).await?; + self.active_camera = Some(cam); log::info!( "Successfully loaded scene with {} entities and {} camera configs", @@ -438,7 +449,7 @@ impl Editor { { let cfg = PROJECT.read(); if cfg.editor_settings.is_debug_menu_shown { - debug::show_menu_bar(ui, &mut self.signal); + debug::show_menu_bar(ui, &mut self.signal, &mut self.dep_installer); } } }); @@ -815,6 +826,7 @@ pub enum Signal { RemoveComponent(hecs::Entity, ComponentType), CreateEntity, LogEntities, + Spawn(PendingSpawn2), } #[derive(Debug)] @@ -967,3 +979,10 @@ pub enum EditorState { Editing, Playing, } + +pub enum PendingSpawn2 { + CreateLight, + CreatePlane, + CreateCube, + CreateCamera, +} @@ -1,10 +1,10 @@ use super::*; -use dropbear_engine::graphics::InstanceRaw; +use dropbear_engine::graphics::{InstanceRaw, RenderContext}; use dropbear_engine::model::Model; use dropbear_engine::starter::plane::PlaneBuilder; use dropbear_engine::{ entity::{AdoptedEntity, Transform}, - graphics::{Graphics, Shader}, + graphics::Shader, lighting::{Light, LightComponent}, model::{DrawLight, DrawModel}, scene::{Scene, SceneCommand}, @@ -13,7 +13,7 @@ use egui::{Align2, Image}; use eucalyptus_core::camera::PlayerCamera; use eucalyptus_core::states::Value; use eucalyptus_core::utils::{PROTO_TEXTURE, PendingSpawn}; -use eucalyptus_core::{logging, model_ext, scripting, success_without_console, warn_without_console}; +use eucalyptus_core::{logging, scripting, success_without_console, warn_without_console}; use log; use parking_lot::Mutex; use wgpu::Color; @@ -23,21 +23,21 @@ use winit::{event_loop::ActiveEventLoop, keyboard::KeyCode}; pub static PENDING_SPAWNS: LazyLock<Mutex<Vec<PendingSpawn>>> = LazyLock::new(|| Mutex::new(Vec::new())); +#[async_trait::async_trait] impl Scene for Editor { - fn load(&mut self, graphics: &mut Graphics) { + async fn load<'a>(&mut self, graphics: &mut RenderContext<'a>) { if self.active_camera.is_none() { - self.load_project_config(graphics).unwrap(); + self.load_project_config(graphics.shared.clone()).await.unwrap(); } let shader = Shader::new( - graphics, + graphics.shared.clone(), include_str!("../../../resources/shaders/shader.wgsl"), Some("viewport_shader"), ); - self.light_manager.create_light_array_resources(graphics); + self.light_manager.create_light_array_resources(graphics.shared.clone()); - let texture_bind_group = &graphics.texture_bind_group().clone(); if let Some(active_camera) = self.active_camera { if let Ok(mut q) = self .world @@ -49,7 +49,7 @@ impl Scene for Editor { let pipeline = graphics.create_render_pipline( &shader, vec![ - texture_bind_group, + &graphics.shared.texture_bind_layout.clone(), camera.layout(), self.light_manager.layout(), ], @@ -58,7 +58,7 @@ impl Scene for Editor { self.render_pipeline = Some(pipeline); self.light_manager.create_render_pipeline( - graphics, + graphics.shared.clone(), include_str!("../../../resources/shaders/light.wgsl"), camera, Some("Light Pipeline"), @@ -79,87 +79,43 @@ impl Scene for Editor { log_once::warn_once!("No active camera found"); } - self.window = Some(graphics.state.window.clone()); + self.window = Some(graphics.shared.window.clone()); } - fn update(&mut self, dt: f32, graphics: &mut Graphics) { + async fn update<'a>(&mut self, dt: f32, graphics: &mut RenderContext<'a>) { if let Some((_, tab)) = self.dock_state.find_active_focused() { self.is_viewport_focused = matches!(tab, EditorTab::Viewport); } else { self.is_viewport_focused = false; } - { + let spawns_to_process = { let mut pending_spawns = PENDING_SPAWNS.lock(); - let mut current_spawn: Option<PendingSpawn> = None; - for spawn in pending_spawns.drain(..) { - if let Some(handle_id) = spawn.handle_id { - match model_ext::GLOBAL_MODEL_LOADER.get_status(handle_id) { - Some(model_ext::ModelLoadingStatus::Loaded) => { - match model_ext::GLOBAL_MODEL_LOADER.exchange_by_id(handle_id) { - Ok(model) => { - let adopted = AdoptedEntity::adopt(graphics, model, Some(&spawn.asset_name)); - let entity_id = Arc::get_mut(&mut self.world).unwrap() - .spawn((adopted, spawn.transform, spawn.properties)); - self.selected_entity = Some(entity_id); - - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::Spawn(entity_id), - ); - - log::info!( - "Successfully spawned {} with ID {:?}", - spawn.asset_name, - entity_id - ); - } - Err(e) => { - log::error!("Failed to exchange model for {}: {}", spawn.asset_name, e); - } - } - } - Some(model_ext::ModelLoadingStatus::Failed(error)) => { - log::error!("Model loading failed for {}: {}", spawn.asset_name, error); - } - Some(model_ext::ModelLoadingStatus::Processing) => { - current_spawn = Some(spawn); - } - Some(model_ext::ModelLoadingStatus::NotLoaded) => { - log::warn!("Model {} not processed yet", spawn.asset_name); - current_spawn = Some(spawn); - } - None => { - log::error!("No handle found for model {}", spawn.asset_name); - } - } - } else { - match AdoptedEntity::new(graphics, &spawn.asset_path, Some(&spawn.asset_name)) { - Ok(adopted) => { - let entity_id = Arc::get_mut(&mut self.world).unwrap() - .spawn((adopted, spawn.transform, spawn.properties)); - self.selected_entity = Some(entity_id); + std::mem::take(&mut *pending_spawns) + }; - UndoableAction::push_to_undo( - &mut self.undo_stack, - UndoableAction::Spawn(entity_id), - ); + for spawn in spawns_to_process { + match AdoptedEntity::new(graphics.shared.clone(), &spawn.asset_path, Some(&spawn.asset_name)).await { + Ok(adopted) => { + let entity_id = Arc::get_mut(&mut self.world).unwrap() + .spawn((adopted, spawn.transform, spawn.properties)); + + self.selected_entity = Some(entity_id); + + UndoableAction::push_to_undo( + &mut self.undo_stack, + UndoableAction::Spawn(entity_id), + ); - log::info!( - "Successfully spawned {} with ID {:?}", - spawn.asset_name, - entity_id - ); - } - Err(e) => { - log::error!("Failed to spawn {}: {}", spawn.asset_name, e); - } - } + log::info!( + "Successfully spawned {} with ID {:?}", + spawn.asset_name, + entity_id + ); + } + Err(e) => { + log::error!("Failed to spawn {}: {}", spawn.asset_name, e); } - } - - if let Some(s) = current_spawn { - pending_spawns.push(s); } } @@ -279,10 +235,10 @@ impl Scene for Editor { match &scene_entity.model_path.to_project_path(self.project_path.clone().unwrap()) { Some(v) => { match AdoptedEntity::new( - graphics, + graphics.shared.clone(), v, Some(&scene_entity.label), - ) { + ).await { Ok(adopted) => { let entity_id = Arc::get_mut(&mut self.world).unwrap().spawn(( adopted, @@ -311,7 +267,7 @@ impl Scene for Editor { }; }, dropbear_engine::utils::ResourceReferenceType::Bytes(bytes) => { - let model = match Model::load_from_memory(graphics, bytes, Some(&scene_entity.label)) { + let model = match Model::load_from_memory(graphics.shared.clone(), bytes, Some(&scene_entity.label)).await { Ok(v) => v, Err(e) => { fatal!("Unable to load from memory: {}", e); @@ -320,10 +276,9 @@ impl Scene for Editor { }, }; let adopted = AdoptedEntity::adopt( - graphics, + graphics.shared.clone(), model, - Some(&scene_entity.label), - ); + ).await; let entity_id = Arc::get_mut(&mut self.world).unwrap().spawn(( adopted, scene_entity.transform, @@ -725,7 +680,7 @@ impl Scene for Editor { .scroll([false, true]) .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) .enabled(true) - .show(&graphics.get_egui_context(), |ui| { + .show(&graphics.shared.get_egui_context(), |ui| { if ui .add_sized( [ui.available_width(), 30.0], @@ -773,7 +728,7 @@ impl Scene for Editor { ); } else { let camera = Camera::predetermined( - graphics, + graphics.shared.clone(), Some(&format!("{} Camera", label)), ); let component = CameraComponent::new(); @@ -813,7 +768,7 @@ impl Scene for Editor { .enabled(true) .open(&mut show) .title_bar(true) - .show(&graphics.get_egui_context(), |ui| { + .show(&graphics.shared.get_egui_context(), |ui| { if ui .add_sized( [ui.available_width(), 30.0], @@ -853,7 +808,7 @@ impl Scene for Editor { .enabled(true) .open(&mut show) .title_bar(true) - .show(&graphics.get_egui_context(), |ui| { + .show(&graphics.shared.get_egui_context(), |ui| { egui_extras::install_image_loaders(ui.ctx()); ui.add(Image::from_bytes( "bytes://theres_nothing", @@ -949,7 +904,7 @@ impl Scene for Editor { .enabled(true) .open(&mut show) .title_bar(true) - .show(&graphics.get_egui_context(), |ui| { + .show(&graphics.shared.get_egui_context(), |ui| { if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Model")).clicked() { log::debug!("Creating new model"); warn!("Instead of using the `Add Entity` window, double click on the imported model in the asset \n\ @@ -959,70 +914,22 @@ impl Scene for Editor { if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Light")).clicked() { log::debug!("Creating new lighting"); - let transform = Transform::new(); - let component = LightComponent::default(); - let light = Light::new(graphics, &component, &transform, Some("Light")); - Arc::get_mut(&mut self.world).unwrap().spawn((light, component, transform)); - success!("Created new light"); - - // always ensure the signal is reset after action is dun - self.signal = Signal::None; + self.signal = Signal::Spawn(PendingSpawn2::CreateLight); } if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Plane")).clicked() { log::debug!("Creating new plane"); - let plane = PlaneBuilder::new() - .with_size(500.0, 200.0) - .build( - graphics, - PROTO_TEXTURE, - Some("Plane") - ).unwrap(); - let transform = Transform::new(); - let mut props = ModelProperties::new(); - props.custom_properties.insert("width".to_string(), Value::Float(500.0)); - props.custom_properties.insert("height".to_string(), Value::Float(200.0)); - props.custom_properties.insert("tiles_x".to_string(), Value::Int(500)); - props.custom_properties.insert("tiles_z".to_string(), Value::Int(200)); - Arc::get_mut(&mut self.world).unwrap().spawn((plane, transform, props)); - success!("Created new plane"); - - self.signal = Signal::None; + self.signal = Signal::Spawn(PendingSpawn2::CreatePlane); } if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Cube")).clicked() { log::debug!("Creating new cube"); - let model = Model::load_from_memory( - graphics, - include_bytes!("../../../resources/cube.glb"), - Some("Cube") - ); - match model { - Ok(model) => { - let cube = AdoptedEntity::adopt( - graphics, - model, - Some("Cube") - ); - Arc::get_mut(&mut self.world).unwrap().spawn((cube, Transform::new(), ModelProperties::new())); - } - Err(e) => { - fatal!("Failed to load cube model: {}", e); - } - } - success!("Created new cube"); - - self.signal = Signal::None; + self.signal = Signal::Spawn(PendingSpawn2::CreateCube); } if ui.add_sized([ui.available_width(), 30.0], egui::Button::new("Camera")).clicked() { log::debug!("Creating new cube"); - let camera = Camera::predetermined(graphics, None); - let component = CameraComponent::new(); - Arc::get_mut(&mut self.world).unwrap().spawn((camera, component)); - success!("Created new camera"); - - self.signal = Signal::None; + self.signal = Signal::Spawn(PendingSpawn2::CreateCamera); } }); if !show { @@ -1042,10 +949,67 @@ impl Scene for Editor { } log::info!("===================="); self.signal = Signal::None; + }, + Signal::Spawn(entity_type) => { + match entity_type { + crate::editor::PendingSpawn2::CreateLight => { + let transform = Transform::new(); + let component = LightComponent::default(); + let light = Light::new(graphics.shared.clone(), &component, &transform, Some("Light")).await; + Arc::get_mut(&mut self.world).unwrap().spawn((light, component, transform)); + success!("Created new light"); + }, + crate::editor::PendingSpawn2::CreatePlane => { + let plane = PlaneBuilder::new() + .with_size(500.0, 200.0) + .build( + graphics.shared.clone(), + PROTO_TEXTURE, + Some("Plane") + ).await.unwrap(); + + let transform = Transform::new(); + let mut props = ModelProperties::new(); + props.custom_properties.insert("width".to_string(), Value::Float(500.0)); + props.custom_properties.insert("height".to_string(), Value::Float(200.0)); + props.custom_properties.insert("tiles_x".to_string(), Value::Int(500)); + props.custom_properties.insert("tiles_z".to_string(), Value::Int(200)); + Arc::get_mut(&mut self.world).unwrap().spawn((plane, transform, props)); + success!("Created new plane"); + }, + crate::editor::PendingSpawn2::CreateCube => { + let model = Model::load_from_memory( + graphics.shared.clone(), + include_bytes!("../../../resources/cube.glb"), + Some("Cube") + ).await; + match model { + Ok(model) => { + let cube = AdoptedEntity::adopt( + graphics.shared.clone(), + model, + // Some("Cube") + ).await; + Arc::get_mut(&mut self.world).unwrap().spawn((cube, Transform::new(), ModelProperties::new())); + } + Err(e) => { + fatal!("Failed to load cube model: {}", e); + } + } + success!("Created new cube"); + }, + crate::editor::PendingSpawn2::CreateCamera => { + let camera = Camera::predetermined(graphics.shared.clone(), None); + let component = CameraComponent::new(); + Arc::get_mut(&mut self.world).unwrap().spawn((camera, component)); + success!("Created new camera"); + }, + } + self.signal = Signal::None; } } - let current_size = graphics.state.viewport_texture.size; + let current_size = graphics.shared.viewport_texture.size; self.size = current_size; let new_aspect = current_size.width as f64 / current_size.height as f64; @@ -1082,12 +1046,12 @@ impl Scene for Editor { .iter() { component.update(camera); - camera.update(graphics); + camera.update(graphics.shared.clone()); } let query = Arc::get_mut(&mut self.world).unwrap().query_mut::<(&mut AdoptedEntity, &Transform)>(); for (_, (entity, transform)) in query { - entity.update(&graphics, transform); + entity.update(graphics.shared.clone(), transform); } let light_query = Arc::get_mut(&mut self.world).unwrap() @@ -1096,10 +1060,12 @@ impl Scene for Editor { light.update(light_component, transform); } - self.light_manager.update(graphics, &Arc::get_mut(&mut self.world).unwrap()); + self.light_manager.update(graphics.shared.clone(), &Arc::get_mut(&mut self.world).unwrap()); + + crate::debug::show_dependency_progress_window(&graphics.shared.get_egui_context(), &mut self.dep_installer); } - fn render(&mut self, graphics: &mut Graphics) { + async fn render<'a>(&mut self, graphics: &mut RenderContext<'a>) { // cornflower blue let color = Color { r: 100.0 / 255.0, @@ -1109,12 +1075,12 @@ impl Scene for Editor { }; self.color = color.clone(); - self.size = graphics.state.viewport_texture.size.clone(); - self.texture_id = Some(graphics.state.texture_id.clone()); - self.show_ui(&graphics.get_egui_context()); + self.size = graphics.shared.viewport_texture.size.clone(); + self.texture_id = Some(*graphics.shared.texture_id.clone()); + self.show_ui(&graphics.shared.get_egui_context()); - self.window = Some(graphics.state.window.clone()); - logging::render(&graphics.get_egui_context()); + self.window = Some(graphics.shared.window.clone()); + logging::render(&graphics.shared.get_egui_context()); if let Some(pipeline) = &self.render_pipeline { if let Some(active_camera) = self.active_camera { if let Ok(mut query) = self.world.query_one::<&Camera>(active_camera) { @@ -1158,7 +1124,7 @@ impl Scene for Editor { for (model_ptr, instances) in model_batches { let model = unsafe { &*model_ptr }; - let instance_buffer = graphics.state.device.create_buffer_init( + let instance_buffer = graphics.shared.device.create_buffer_init( &wgpu::util::BufferInitDescriptor { label: Some("Batched Instance Buffer"), contents: bytemuck::cast_slice(&instances), @@ -16,7 +16,8 @@ pub const APP_INFO: app_dirs2::AppInfo = app_dirs2::AppInfo { author: "4tkbytes", }; -fn main() -> anyhow::Result<()> { +#[tokio::main] +async fn main() -> anyhow::Result<()> { #[cfg(target_os = "android")] compile_error!( "The `editor` feature is not supported on Android. If you are attempting\ @@ -5,8 +5,7 @@ use std::{ use anyhow::anyhow; use dropbear_engine::{ - input::{Controller, Keyboard, Mouse}, - scene::{Scene, SceneCommand}, + graphics::RenderContext, input::{Controller, Keyboard, Mouse}, scene::{Scene, SceneCommand} }; use egui::{self, FontId, Frame, RichText}; use egui_toast_fork::{ToastOptions, Toasts}; @@ -147,19 +146,20 @@ impl MainMenu { } } +#[async_trait::async_trait] impl Scene for MainMenu { - fn load(&mut self, _graphics: &mut dropbear_engine::graphics::Graphics) { + async fn load<'a>(&mut self, _graphics: &mut RenderContext<'a>) { log::info!("Loaded menu scene"); } - fn update(&mut self, _dt: f32, _graphics: &mut dropbear_engine::graphics::Graphics) {} + async fn update<'a>(&mut self, _dt: f32, _graphics: &mut RenderContext<'a>) {} - fn render(&mut self, graphics: &mut dropbear_engine::graphics::Graphics) { + async fn render<'a>(&mut self, graphics: &mut RenderContext<'a>) { let screen_size: (f32, f32) = ( - graphics.state.window.inner_size().width as f32 - 100.0, - graphics.state.window.inner_size().height as f32 - 100.0, + graphics.shared.window.inner_size().width as f32 - 100.0, + graphics.shared.window.inner_size().height as f32 - 100.0, ); - let egui_ctx = graphics.get_egui_context(); + let egui_ctx = graphics.shared.get_egui_context(); egui::CentralPanel::default() .frame(Frame::new()) @@ -325,7 +325,7 @@ impl Scene for MainMenu { }); } - self.toast.show(&graphics.get_egui_context()); + self.toast.show(&graphics.shared.get_egui_context()); } fn exit(&mut self, _event_loop: &ActiveEventLoop) { @@ -0,0 +1,9 @@ +name = "dropbear" +version = "1.0.0" +target = "javascript" + +[javscript] +runtime = "node" + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" @@ -0,0 +1,9 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.63.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "E1D5EC07638F606E48F0EA1556044DD805F2ACE9092A6F6AFBE4A0CC4DA21C2F" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } @@ -1 +1 @@ -Subproject commit 5cb98c0d508c7f8fd7d6a268d101d6e0fde0f41f +Subproject commit 878892a1a56f9e3b9d25f3bd7ba658e2b2a051cc @@ -1,1115 +0,0 @@ -// Modules for the dropbear-engine scripting component -// Made by 4tkbytes -// EDIT THIS IF YOU WISH, I RECOMMEND YOU NOT TOUCH IT - -/** - * A class describing the position, scale and rotation to be able to manipulate - * the entity's location. - */ -export class Transform { - /** - * A {@link Vector3} describing the position of the entity - */ - position: Vector3; - /** - * A {@link Quaternion} describing the rotation of the entity - */ - rotation: Quaternion; - /** - * A {@link Vector3} describing the scale of the entity - */ - scale: Vector3; - - public constructor(position?: Vector3, rotation?: Quaternion, scale?: Vector3) { - this.position = position || Vector3.zero(); - this.rotation = rotation || Quaternion.identity(); - this.scale = scale || Vector3.one(); - } - - /** - * Translates/Offsets the position of the entity by a {@link Vector3} - * - * # Example - * @example - * ```ts - * let transform = new Transform(); - * transform.translate(new Vector3(1.0, 1.0, 1.0)); - * ``` - * - * @param movement - A {@link Vector3} for position - */ - public translate(movement: Vector3) { - this.position.x += movement.x; - this.position.y += movement.y; - this.position.z += movement.z; - } - - /** - * Rotates the transformables rotation on the X axis - * - * # Example - * @example - * ```ts - * let transform = new Transform(); - * transform.rotateX(dbMath.degreesToRadians(180)); - * ``` - * - * @param angle - The angle in radians - */ - public rotateX(angle: number) { - const rotQuat = Quaternion.fromAxisAngle(new Vector3(1, 0, 0), angle); - this.rotation = Quaternion.multiply(this.rotation, rotQuat); - - } - - /** - * Rotates the transformables rotation on the Y axis - * - * # Example - * ```ts - * let transform = new Transform(); - * transform.rotateY(dbMath.degreesToRadians(180)) - * ``` - * - * @param angle - The angle in radians - */ - public rotateY(angle: number) { - const rotQuat = Quaternion.fromAxisAngle(new Vector3(0, 1, 0), angle); - this.rotation = Quaternion.multiply(this.rotation, rotQuat); - } - - /** - * Rotates the transformables rotation on the Z axis - * - * # Example - * ```ts - * let transform = new Transform(); - * transform.rotateZ(dbMath.degreesToRadians(180)) - * ``` - * - * @param angle - The angle in radians - */ - public rotateZ(angle: number) { - const rotQuat = Quaternion.fromAxisAngle(new Vector3(0, 0, 1), angle); - this.rotation = Quaternion.multiply(this.rotation, rotQuat); - } - - /** - * Uniformly scales the entity by a multiplier. - * - * # Example - * ```ts - * let transform = new Transform(); - * transform.scaleUniform(2.0) - * ``` - * - * @param scale - A number that the scale multiplies by - */ - public scaleUniform(scale: number) { - this.scale.x *= scale; - this.scale.y *= scale; - this.scale.z *= scale; - } - - /** - * Individually scales the entity by a multiplier by using a {@link Vector3} - * - * # Example - * ```ts - * let transform = new Transform(); - * transform.scaleIndividual(new Vector3(1.0, 2.0, 1.5)) - * ``` - * - * @param scale - A Vector3 representing the scale.x, scale.y and scale.z values - */ - public scaleIndividual(scale: Vector3) { - this.scale.x *= scale.x; - this.scale.y *= scale.y; - this.scale.z *= scale.z; - } -} - -/** - * Utilities for math functions that do not exist in the TypeScript Math module. - */ -export const dbMath = { - /** - * Convert from degrees to radians - * - * # Example - * @example - * ```ts - * console.log(dbMath.degreesToRadians(180)) // expect Math.PI - * ``` - * - * @param deg - The angle in degrees - * @returns The angle in radians - */ - degreesToRadians:(deg: number):number => { - return deg * (Math.PI / 180.0); - }, - - /** - * Convert from radians to degrees - * - * # Example - * @example - * ```ts - * console.log(dbMath.radiansToDegrees(Math.PI)) // expect 180 - * ``` - * - * @param rad - The angle in radians - * @returns The angle in degrees - */ - radiansToDegrees:(rad: number):number => { - return (180 * rad) / Math.PI; - }, - - /** - * Constrains a number to lie within a specified range. If value is less than min, returns min. - * If value is greater than max, returns max. Otherwise, returns value. - * - * @example - * ```ts - * dropbear.dbMath.clamp(5, 0, 10) // → 5 - * dropbear.dbMath.clamp(-3, 0, 10) // → 0 - * dropbear.dbMath.clamp(15, 0, 10) // → 10 - * ``` - * - * @param value - The input value to clamp - * @param min - The lower bound of the range - * @param max - The upper bound of the range - * @returns - The clamped value - */ - clamp: (value: number, min: number, max: number): number => { - return Math.min(Math.max(value, min), max); - } -} - -/** - * A Vector of 3 components: an X, Y and Z. - */ -export class Vector3 { - /** - * The X value - */ - public x: number; - - /** - * The Y value - */ - public y: number; - /** - * The Z value - */ - public z: number; - - public constructor(x: number, y: number, z: number) { - this.x = x; - this.y = y; - this.z = z; - } - - /** - * Converts the {@link Vector3} class to a primitive number array - * - * # Example - * @example - * ```ts - * let vec = new Vector3(1.0, 1.5, 2.0); - * console.log(vec.as_array()) // expect [1.0, 1.5, 2.0] - * ``` - * - * @returns A number array representing the x, y, z values - */ - public as_array(): [number, number, number] { - return [this.x, this.y, this.z]; - } - - /** - * An alternative static constructor to create a {@link Vector3} with all values - * set to 0.0 - * - * # Example - * ```ts - * let vec = Vector3.zero(); - * console.log(vec); // expect [0.0, 0.0, 0.0] - * ``` - * - * @returns A new Vector3 instance with all values set to 0.0 - */ - public static zero(): Vector3 { - return new Vector3(0.0, 0.0, 0.0); - } - - /** - * An alternative static constructor to create a {@link Vector3} with all values - * set to 1.0 - * - * # Example - * ```ts - * let vec = Vector3.one(); - * console.log(vec); // expect [1.0, 1.0, 1.0] - * ``` - * - * @returns A new Vector3 instance with all values set to 1.0 - */ - public static one(): Vector3 { - return new Vector3(1.0, 1.0, 1.0); - } - - /** - * An alternative static constructor that creates a Vector3 from a number array. - * - * # Example - * @example - * ```ts - * let arr = [1.0, 1.5, 2.0]; - * let vec = Vector3.fromArray(arr); - * console.log(vec.to_array() === arr) // expect to print 'true' - * ``` - * - * @param arr - The number array to convert - * @returns - A new Vector3 instance - */ - public static fromArray(arr: [number, number, number]): Vector3 { - return new Vector3(arr[0], arr[1], arr[2]); - } - - /** - * Add another Vector3 to this vector and return the result as a new Vector3. - * - * @param rhs - Right-hand side vector to add. - * @returns A new Vector3 equal to (this + rhs). - * - * @example - * const a = new Vector3(1, 2, 3); - * const b = new Vector3(4, 5, 6); - * const c = a.add(b); // Vector3(5,7,9) - */ - public add(rhs: Vector3): Vector3 { - return new Vector3(this.x + rhs.x, this.y + rhs.y, this.z + rhs.z); - } - - /** - * Subtract another Vector3 from this vector and return the result as a new Vector3. - * - * @param rhs - Right-hand side vector to subtract. - * @returns A new Vector3 equal to (this - rhs). - * - * @example - * const a = new Vector3(5, 7, 9); - * const b = new Vector3(1, 2, 3); - * const c = a.subtract(b); // Vector3(4,5,6) - */ - public subtract(rhs: Vector3): Vector3 { - return new Vector3(this.x - rhs.x, this.y - rhs.y, this.z - rhs.z); - } - - /** - * Multiply this vector by a scalar and return the result as a new Vector3. - * - * @param rhs - Scalar multiplier. - * @returns A new Vector3 scaled by rhs. - * - * @example - * const v = new Vector3(1, 2, 3); - * const s = v.multiply(2); // Vector3(2,4,6) - */ - public multiply(rhs: number): Vector3 { - return new Vector3(this.x * rhs, this.y * rhs, this.z * rhs); - } - - /** - * Compute the Euclidean length (magnitude) of this vector. - * - * @returns The length sqrt(x*x + y*y + z*z). - * - * @example - * const v = new Vector3(1, 2, 2); - * console.log(v.length()); // 3 - */ - public length(): number { - return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); - } - - /** - * Return a new Vector3 representing the normalized (unit) direction of this vector. - * If the vector has zero length, returns Vector3.zero(). - * - * @returns A normalized Vector3 or Vector3.zero() when length is 0. - * - * @example - * const v = new Vector3(0, 3, 4); - * const n = v.normalize(); // Vector3(0, 0.6, 0.8) - */ - public normalize(): Vector3 { - const len = this.length(); - if (len === 0) return Vector3.zero(); - return new Vector3(this.x / len, this.y / len, this.z / len); - } -} - -/** - * Quaternion representing a rotation in 3D space. - * - * Components are stored as (x, y, z, w) where w is the scalar part. - * Useful helpers are provided to construct quaternions from Euler angles, - * axis/angle, multiply them, and convert to/from arrays. - */ -export class Quaternion { - public x: number; - public y: number; - public z: number; - public w: number; - - /** - * Create a new quaternion. - * - * @param x - X component (default 0) - * @param y - Y component (default 0) - * @param z - Z component (default 0) - * @param w - W (scalar) component (default 1) - * - * @example - * const q = new Quaternion(); // identity - * const q2 = new Quaternion(0.1, 0.2, 0.3, 0.9); - */ - public constructor(x: number = 0, y: number = 0, z: number = 0, w: number = 1) { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - - /** - * Construct a quaternion from Euler angles (radians). - * - * The parameters represent rotations about the X, Y and Z axes respectively. - * Angles must be provided in radians. - * - * @param x - rotation about the X axis in radians - * @param y - rotation about the Y axis in radians - * @param z - rotation about the Z axis in radians - * @returns A new Quaternion representing the composite rotation - * - * @example - * const q = Quaternion.fromEuler(Math.PI/2, 0, 0); // 90° around X - */ - public static fromEuler(x: number, y: number, z: number): Quaternion { - // Convert euler angles to quaternion - const cx = Math.cos(x * 0.5); - const sx = Math.sin(x * 0.5); - const cy = Math.cos(y * 0.5); - const sy = Math.sin(y * 0.5); - const cz = Math.cos(z * 0.5); - const sz = Math.sin(z * 0.5); - - return new Quaternion( - sx * cy * cz - cx * sy * sz, - cx * sy * cz + sx * cy * sz, - cx * cy * sz - sx * sy * cz, - cx * cy * cz + sx * sy * sz - ); - } - - /** - * Construct a quaternion representing a rotation around an axis. - * - * The axis will be normalized internally. Angle is in radians. - * - * @param axis - Rotation axis as a Vector3 - * @param angle - Rotation angle in radians - * @returns A new Quaternion representing the axis-angle rotation - * - * @example - * const q = Quaternion.fromAxisAngle(new Vector3(0,1,0), Math.PI); // 180° around Y - */ - public static fromAxisAngle(axis: Vector3, angle: number): Quaternion { - const halfAngle = angle * 0.5; - const sin = Math.sin(halfAngle); - const normalizedAxis = axis.normalize(); - - return new Quaternion( - normalizedAxis.x * sin, - normalizedAxis.y * sin, - normalizedAxis.z * sin, - Math.cos(halfAngle) - ); - } - - /** - * Multiply two quaternions. - * - * The result corresponds to the composition a * b (apply b, then a) using - * the multiplication implemented here. - * - * @param a - Left quaternion - * @param b - Right quaternion - * @returns The product quaternion - * - * @example - * const r = Quaternion.multiply(q1, q2); - */ - public static multiply(a: Quaternion, b: Quaternion): Quaternion { - return new Quaternion( - a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, - a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x, - a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w, - a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z - ); - } - - /** - * Create a quaternion from a numeric array [x, y, z, w]. - * - * @param arr - Array containing quaternion components in order [x, y, z, w] - * @returns A new Quaternion with components taken from the array - * - * @example - * const q = Quaternion.fromArray([0, 0, 0, 1]); - */ - public static fromArray(arr: [number, number, number, number]): Quaternion { - return new Quaternion(arr[0], arr[1], arr[2], arr[3]); - } - - /** - * Return the identity quaternion (no rotation). - * - * @returns Quaternion equal to (0, 0, 0, 1) - * - * @example - * const id = Quaternion.identity(); - */ - public static identity(): Quaternion { - return new Quaternion(0.0, 0.0, 0.0, 1.0); - } - - /** - * Convert this quaternion to a numeric array [x, y, z, w]. - * - * @returns An array containing the quaternion components - * - * @example - * const arr = q.as_array(); // [x, y, z, w] - */ - public as_array(): [number, number, number, number] { - return [this.x, this.y, this.z, this.w]; - } -} - -/** - * Values of the Key codes. - */ -export const Keys = { - KeyA: "KeyA", KeyB: "KeyB", KeyC: "KeyC", KeyD: "KeyD", KeyE: "KeyE", KeyF: "KeyF", - KeyG: "KeyG", KeyH: "KeyH", KeyI: "KeyI", KeyJ: "KeyJ", KeyK: "KeyK", KeyL: "KeyL", - KeyM: "KeyM", KeyN: "KeyN", KeyO: "KeyO", KeyP: "KeyP", KeyQ: "KeyQ", KeyR: "KeyR", - KeyS: "KeyS", KeyT: "KeyT", KeyU: "KeyU", KeyV: "KeyV", KeyW: "KeyW", KeyX: "KeyX", - KeyY: "KeyY", KeyZ: "KeyZ", - Digit0: "Digit0", Digit1: "Digit1", Digit2: "Digit2", Digit3: "Digit3", Digit4: "Digit4", - Digit5: "Digit5", Digit6: "Digit6", Digit7: "Digit7", Digit8: "Digit8", Digit9: "Digit9", - Space: "Space", - ShiftLeft: "ShiftLeft", ShiftRight: "ShiftRight", - ControlLeft: "ControlLeft", ControlRight: "ControlRight", - AltLeft: "AltLeft", AltRight: "AltRight", - Escape: "Escape", Enter: "Enter", Tab: "Tab", - ArrowUp: "ArrowUp", ArrowDown: "ArrowDown", ArrowLeft: "ArrowLeft", ArrowRight: "ArrowRight", - F1: "F1", F2: "F2", F3: "F3", F4: "F4", F5: "F5", F6: "F6", - F7: "F7", F8: "F8", F9: "F9", F10: "F10", F11: "F11", F12: "F12", -} as const; - -export type KeyCode = typeof Keys[keyof typeof Keys]; - -/** - * The properties of the entity. - * From the eucalyptus editor, you are able to make custom - * properties such as 'speed' or 'health'. This class allows - * you to create and edit new value - * during the runtime of the script. - * - * Usage: - * ```ts - * props.setNumber("hp", 100); - * const hp = props.getNumber("hp"); // 100 - * ``` - */ -export class EntityProperties { - private data: Record<string, any>; - - /** - * Create an EntityProperties wrapper. - * - * @param data - Optional initial properties object. A shallow reference is kept. - */ - public constructor(data?: Record<string, any>) { - this.data = data || {}; - } - - /** - * Get the raw underlying properties object. - * - * Use this to serialize properties back to the host or perform bulk operations. - * - * @returns The raw Record<string, any> used to store properties. - * - * @example - * const raw = props.getRawProperties(); - */ - public getRawProperties(): Record<string, any> { - return this.data; - } - - /** - * Set a string property. - * - * @param key - Property key. - * @param value - String value to set. - * - * @example - * props.setString("tag", "friendly"); - */ - public setString(key: string, value: string): void { - this.data[key] = value; - } - - /** - * Set a numeric property. - * - * @param key - Property key. - * @param value - Number value to set. - * - * @example - * props.setNumber("speed", 4.2); - */ - public setNumber(key: string, value: number): void { - this.data[key] = value; - } - - /** - * Set a boolean property. - * - * @param key - Property key. - * @param value - Boolean value to set. - * - * @example - * props.setBool("isActive", true); - */ - public setBool(key: string, value: boolean): void { - this.data[key] = value; - } - - /** - * Get a string property. - * - * If the property is missing or falsy, an empty string ("") is returned. - * - * @param key - Property key. - * @returns The string value or "" when missing. - * - * @example - * const name = props.getString("name"); - */ - public getString(key: string): string { - return this.data[key] || ""; - } - - /** - * Get a numeric property. - * - * If the property is missing or falsy, 0 is returned. - * - * @param key - Property key. - * @returns The number value or 0 when missing. - * - * @example - * const speed = props.getNumber("speed"); - */ - public getNumber(key: string): number { - return this.data[key] || 0; - } - - /** - * Get a boolean property. - * - * If the property is missing or falsy, false is returned. - * - * @param key - Property key. - * @returns The boolean value or false when missing. - * - * @example - * const active = props.getBool("isActive"); - */ - public getBool(key: string): boolean { - return this.data[key] || false; - } - - /** - * Checks if the entity has a specific property as per a key. - * - * If the property exists, it will return true. If not, it will return false - * @param key - Property key. - * @returns - True is the value exists, false if not - * - * @example - * ```ts - * if props.hasProperty("speed") { - * let speed = props.getNumber("speed"); - * speed = speed+10; - * } else { - * console.log("No value as speed exists"); - * } - * ``` - */ - public hasProperty(key: string): boolean { - return key in this.data; - } -} - -export class Entity { - public label: string; - public transform: Transform; - public properties: EntityProperties; - - /** - * Creates a new instance of entity - * @param entityData - The entty data, typically parsed as an argument in the load or update functions - */ - constructor(label: string, properties: EntityData, transform: TransformData) { - this.label = label; - this.transform = createTransformFromData(transform); - this.properties = new EntityProperties(properties); - } - - /** - * Moves the player forward on the Z axis - * @param distance - The distance (as a number) it moves forward by - */ - moveForward(distance: number): void { - const movement = new Vector3(0, 0, -distance); - this.transform.translate(movement); - } - - /** - * Moves the player back on the Z axis - * @param distance - The distance (as a number) it moves back by - */ - moveBack(distance: number): void { - const movement = new Vector3(0, 0, distance); - this.transform.translate(movement); - } - - /** - * Moves the player left on the X axis - * @param distance - The distance (as a number) it moves left by - */ - moveLeft(distance: number): void { - const movement = new Vector3(-distance, 0, 0); - this.transform.translate(movement); - } - - /** - * Moves the player right on the X axis - * @param distance - The distance (as a number) it moves right by - */ - moveRight(distance: number): void { - const movement = new Vector3(distance, 0, 0); - this.transform.translate(movement); - } - - /** - * Moves the player up on the Y axis - * @param distance - The distance (as a number) it moves up by - */ - moveUp(distance: number): void { - const movement = new Vector3(0, distance, 0); - this.transform.translate(movement); - } - - /** - * Moves the player down on the Y axis - * @param distance - The distance (as a number) it moves down by - */ - moveDown(distance: number): void { - const movement = new Vector3(0, -distance, 0); - this.transform.translate(movement); - } -} - - -/** - * Helper function that creates a new {@link Transform} from - * a {@link TransformData} - * @param data - The raw transformable ({@link TransformData})data - * @returns - An instance of a {@link Transform} - */ -function createTransformFromData(data: TransformData): Transform { - const position = Vector3.fromArray(data.position); - const rotation = Quaternion.fromArray(data.rotation); - const scale = Vector3.fromArray(data.scale); - return new Transform(position, rotation, scale); -} - -/** - * Camera class for controlling and manipulating cameras in the scene. - * Provides functionality for camera movement, switching, and property manipulation. - */ -export class Camera { - public label: string; - - public eye: Vector3; - public target: Vector3; - public up: Vector3; - public aspect: number; - public fov: number; - public near: number; - public far: number; - public yaw: number; - public pitch: number; - public speed: number; - public sensitivity: number; - public camera_type: string; - - - /** - * Create a new Camera instance. - * - * @param data - Optional camera data to initialize from - */ - constructor(label: string, data: CameraData) { - this.eye = Vector3.fromArray(data.eye); - this.target = Vector3.fromArray(data.target); - this.up = Vector3.fromArray(data.up); - this.aspect = data.aspect; - this.fov = data.fov; - this.near = data.near; - this.far = data.far; - this.yaw = data.yaw; - this.pitch = data.pitch; - this.speed = data.speed; - this.sensitivity = data.sensitivity; - this.camera_type = data.camera_type; - this.label = label; - } - - /** - * Track mouse movement for camera look controls. - * - * @param delta - Mouse delta X and Y - * - * @example - * ```ts - * let delta = entity.getMouseDelta(); - * camera.track - * ``` - */ - trackMouseDelta(delta: [number, number]): void { - this.yaw += delta[0] * this.sensitivity; - this.pitch += delta[1] * this.sensitivity; - - this.pitch = dbMath.clamp(this.pitch, dbMath.degreesToRadians(-89.0), dbMath.degreesToRadians(89.0)); - - - let direction = new Vector3( - Math.cos(this.yaw) * Math.cos(this.pitch), - Math.sin(this.pitch), - Math.sin(this.yaw) * Math.cos(this.pitch), - ); - this.target = this.eye.add(direction); - } -} - -export class Light { - -} - -/** - * A raw format for storing the transform data, typically used as a - * FFI by the engine - */ -export interface TransformData { - position: [number, number, number]; - rotation: [number, number, number, number]; // quaternion [x, y, z, w] - scale: [number, number, number]; -} - -/** - * A raw format for storing the custom entity properties from the engines - * raw FFI - */ -export interface EntityData { - custom_properties: Record<string, any>; -} - -/** - * A raw format for storing the input data - */ -export interface InputData { - mouse_pos: [number, number]; - pressed_keys: string[]; - mouse_delta: [number, number] | null; - is_cursor_locked: boolean; -} - -/** - * A raw format for storing the camera data - */ -export interface CameraData { - eye: [number, number, number]; - target: [number, number, number]; - up: [number, number, number]; - aspect: number; - fov: number; - near: number; - far: number; - yaw: number; - pitch: number; - speed: number; - sensitivity: number; - camera_type: string; -} - -/** - * A raw format for storing the light data - */ -export interface LightData { - -} - -/** - * A raw format for storing the scene data - */ -export interface RawSceneData { - entities: [{ - label: string, - properties: EntityData, - transform: TransformData - }]; - cameras: [{ - label: string, - data: CameraData - }]; - lights: [{ - label: string, - data: LightData - }]; - input: InputData; -} - -/** - * A wrapper class aimed to aid with input data - */ -export class Input { - public inputData?: InputData - - // constructor(data: InputData) { - // this.inputData = data; - // } - - // empty because global variable - constructor() { - - } - - /** - * Checks if a key is pressed - * @param key - The specific keycode - * @returns - True is pressed, false if not - * - * @example - * ```ts - * if entity.isKeyPressed(Keys::KeyW) { - * console.log("The W key is pressed"); - * } - * ``` - */ - isKeyPressed(key: KeyCode): boolean { - if (!this.inputData) return false; - return this.inputData.pressed_keys.indexOf(key) !== -1 - } - - /** - * Fetches the mouse position - * @returns - The x,y position of the mouse - */ - getMousePosition(): [number, number] { - return this.inputData?.mouse_pos || [0, 0]; - } - - /** - * Fetches the change in the mouse position from the center (as it gets reset each frame) - * @returns - The dx,dy position of the mouse - */ - getMouseDelta(): [number, number] { - return this.inputData?.mouse_delta || [0.0, 0.0]; - } -} - -/** - * The class containing all the different entities in this scene. - */ -export class Scene { - // ensure none of these are public - current_entity?: string; - entities?: Entity[]; - cameras?: Camera[]; - lights?: Light[]; - - // Sets everything to default - constructor() { - this.cameras = []; - this.entities = []; - this.lights = []; - } - - /** - * Returns a **reference** to the camera in the scene - * @param label - The label of the camera as set by you from the editor - */ - public getCamera(label: string): Camera | undefined { - return this.cameras?.find(c => c.label === label); - } - - /** - * Returns a **reference** to the light in the scene - * @param label - The label of the light as set by you from the editor - */ - public getLight(label: string): Light | undefined { - return this.lights?.find(l => (l as any).label === label); - } - - /** - * Fetches an entity as per its label. Returns the actual Entity instance (mutable reference). - * @param label - The label of the entity - */ - public getEntity(label: string): Entity | undefined { - return this.entities?.find(e => e.label === label); - } - - /** - * Fetches the current entity this script is attached to (returns mutable reference). - */ - public getCurrentEntity(): Entity | undefined { - if (!this.current_entity) { - console.error("Unable to get entity: Have you added dropbear.start(s) yet?"); - return undefined; - } - const ent = this.getEntity(this.current_entity); - if (!ent) { - console.error(`Unable to get entity: no entity with label "${this.current_entity}" found`); - } - return ent; - } -} - -/** - * Starts the specific function by filling the scene data. - * @param data - */ -export function start(data: RawSceneData) { - data.cameras.forEach(camera => { - scene.cameras?.push(new Camera(camera.label, camera.data)) - }); - data.entities.forEach(entity => { - scene.entities?.push(new Entity(entity.label, entity.properties, entity.transform)) - }); - data.lights.forEach(light => { - scene.lights?.push(light) - }); - input.inputData = data.input; -} - -/** - * Ends the scripting function by returning a Partial RawSceneData for the - * rust client to take. - */ -export function end(): Partial<RawSceneData> { - const out: Partial<RawSceneData> = {}; - - if (scene.entities && scene.entities.length) { - out.entities = scene.entities.map(e => { - return { - label: e.label, - properties: { - custom_properties: e.properties.getRawProperties() - } as EntityData, - transform: { - position: e.transform.position.as_array(), - rotation: e.transform.rotation.as_array(), - scale: e.transform.scale.as_array() - } as TransformData - }; - }) as any; - } - - if (scene.cameras && scene.cameras.length) { - out.cameras = scene.cameras.map(c => { - return { - label: c.label, - data: { - eye: c.eye.as_array(), - target: c.target.as_array(), - up: c.up.as_array(), - aspect: c.aspect, - fov: c.fov, - near: c.near, - far: c.far, - yaw: c.yaw, - pitch: c.pitch, - speed: c.speed, - sensitivity: c.sensitivity, - camera_type: c.camera_type - } as CameraData - }; - }) as any; - } - - if (scene.lights && scene.lights.length) { - out.lights = scene.lights.map(l => { - return { - label: (l as any).label || "", - data: {} as LightData - }; - }) as any; - } - - if (input.inputData) { - out.input = input.inputData; - } - - return out; -} - -// global variables -/** - * A global variable that contains all the information about the scene. - * - * To use the variable, you need to run {@link start()} at the start of - * the function and return {@link end()} at the end to send to the engine. - * - * @example - * ```ts - * export function onUpdate(s, dt: number) { - * dropbear.start(s); - * console.log("I'm being updated!"); - * return dropbear.end(); - * } - * ``` - */ -export const scene = new Scene(); -/** - * A global variable that contains all the information about the inputs - * such as the keyboard and the mouse. - * - * To use the variable, you need to run {@link start()} at the start of - * the function and return {@link end()} at the end to send to the engine. - * - * @example - * ```ts - * export function onUpdate(s, dt: number) { - * dropbear.start(s); - * console.log("I'm being updated!"); - * return dropbear.end(); - * } - */ -export const input = new Input(); @@ -1,25 +0,0 @@ -// dropbear-engine script template for eucalyptus -import * as dropbear from "./dropbear"; - -export function onLoad(s: dropbear.RawSceneData) { - dropbear.start(s); - // ------- Your own code here ------- - console.log("I have been awoken"); - - - // ---------------------------------- - // Do not remove anything outside unless - // you know what you are doing. - return dropbear.end(); -} - -export function onUpdate(s: dropbear.RawSceneData, dt: number) { - dropbear.start(s); - // ------- Your own code here ------- - console.log("I'm being updated!"); - - - // ---------------------------------- - // Same thing over here! - return dropbear.end(); -} @@ -0,0 +1,2 @@ +@external(javascript, "dropbear_input", "is_key_pressed") +fn is_key_pressed(key_code: Int) -> Int @@ -0,0 +1,57 @@ +/// The generic type of an entity, containing a position, rotation and scale. +pub type Transform { + Transform ( + position: Vector3(Float), + rotation: Quaternion(Float), + scale: Vector3(Float), + ) +} + +/// Creates a new transform +pub fn new_transform() -> Transform { + Transform( + position: zero_vector3f(), + rotation: identity_quatf(), + scale: zero_vector3f(), + ) +} + +/// A type used to show 3 instances of a value. +pub type Vector3(a) { + Vector3( + /// X value + x: a, + /// Y value + y: a, + /// Z value + z: a, + ) +} + +/// Creates a new Vector3(Float) of all 0.0. +pub fn zero_vector3f() -> Vector3(Float) { + Vector3( + x: 0.0, + y: 0.0, + z: 0.0, + ) +} + +pub type Quaternion(a) { + Quaternion( + w: a, + x: a, + y: a, + z: a, + ) +} + +/// Creates a new quaternion with 1.0 as the scale and 0.0 for the x, y and z. +pub fn identity_quatf() -> Quaternion(Float) { + Quaternion( + w: 1.0, + x: 0.0, + y: 0.0, + z: 0.0, + ) +} @@ -0,0 +1 @@ +//// Internal library for WASM memory sharing between the dropbear game engine. @@ -0,0 +1,14 @@ +/// Javascript library for managing memory in WebAssembly for the dropbear engine Gleam interface. + +class WasmMemoryManager { + constructor(memory) { + this.memory = memory; + this.nextFreeOffset = 1024; + this.allocations = new Map(); + } + + /// Fetches the new view of the memory after the memory is refreshed + getView() { + return new DataView(this.memory.buffer); + } +}