(&self) {
+ let type_id = TypeId::of::();
+ let pipeline = Arc::new(RwLock::new(P::default()));
+
+ let mut pipelines_id = self.pipelines_id.write().unwrap();
+ let mut pipelines_state = self.pipelines_state.write().unwrap();
+ let mut pipelines = self.pipelines.write().unwrap();
+
+ pipelines_id.push(type_id);
+ pipelines_state.push(Arc::new(RwLock::new(MaterialState::Loading)));
+ pipelines.push(pipeline.clone());
+ }
+
+ pub fn add_material(&self) -> Result<(), MaterialError> {
+ let pipeline_id = M::pipeline_type_id();
+
+ let pipeline_result = {
+ let pipelines_id = self.pipelines_id.read().unwrap();
+ let pipelines_state = self.pipelines_state.read().unwrap();
+ let pipelines = self.pipelines.read().unwrap();
+
+ pipelines_id
+ .iter()
+ .zip(pipelines.iter())
+ .zip(pipelines_state.iter())
+ .find(|((id, _), _)| *id == &pipeline_id)
+ .map(|((_, pipeline), state)| (pipeline.clone(), state.clone()))
+ };
+
+ let (pipeline, pipeline_state) = match pipeline_result {
+ Some(pipeline) => pipeline,
+ None => {
+ tracing::error!(
+ "Pipeline with id {pipeline_id:?} not found, please add it before adding a material"
+ );
+ return Err(MaterialError::PipelineNotFound);
+ }
+ };
+
+ let type_id = TypeId::of::();
+
+ let mut materials_id = self.materials_id.write().unwrap();
+ let mut materials_pipeline_id = self.materials_pipeline_id.write().unwrap();
+ let mut materials_pipeline = self.materials_pipeline.write().unwrap();
+ let mut materials_pipeline_state = self.materials_pipeline_state.write().unwrap();
+ let mut materials_state = self.materials_state.write().unwrap();
+ let mut materials = self.materials.write().unwrap();
+
+ materials_id.push(type_id);
+ materials_pipeline_id.push(pipeline_id);
+ materials_pipeline.push(pipeline);
+ materials_pipeline_state.push(pipeline_state);
+ materials_state.push(Arc::new(RwLock::new(MaterialState::Loading)));
+ materials.push(Arc::new(RwLock::new(M::default())));
+
+ Ok(())
+ }
+
+ pub fn update_swapchain_format(&mut self, swapchain_format: Format) {
+ if self.swapchain_format == swapchain_format {
+ return;
+ }
+
+ self.swapchain_format = swapchain_format;
+ self.mark_all_pipelines_as_loading();
+ }
+
+ fn mark_all_pipelines_as_loading(&self) {
+ let pipelines_state = self.pipelines_state.write().unwrap();
+
+ for state in pipelines_state.iter() {
+ let mut state = state.write().unwrap();
+ *state = MaterialState::Loading;
+ }
+ }
+
+ fn load_pipelines(&self) {
+ let pipelines_state = self.pipelines_state.read().unwrap();
+ let pipelines = self.pipelines.read().unwrap();
+
+ let iter = pipelines_state
+ .iter()
+ .zip(pipelines.iter())
+ .filter(|(state, _)| {
+ let state = state.read().unwrap();
+ matches!(*state, MaterialState::Loading)
+ });
+
+ for (state, pipeline) in iter {
+ let mut pipeline = pipeline.write().unwrap();
+ let result = pipeline.load(
+ &self.device,
+ &self.memory_allocator,
+ self.swapchain_format,
+ self.depth_format,
+ );
+
+ match result {
+ Ok(_) => {
+ let mut state = state.write().unwrap();
+ *state = MaterialState::Loaded;
+ }
+ Err(e) => {
+ tracing::error!("Failed to load pipeline: {e}");
+ }
+ }
+ }
+ }
+
+ fn load_materials(&self) {
+ let materials_state = self.materials_state.read().unwrap();
+ let materials = self.materials.read().unwrap();
+
+ let iter = materials_state
+ .iter()
+ .zip(materials.iter())
+ .filter(|(state, _)| {
+ let state = state.read().unwrap();
+ matches!(*state, MaterialState::Loading)
+ });
+
+ for (state, material) in iter {
+ let mut material = material.write().unwrap();
+ let result = material.load(&self.device, &self.memory_allocator);
+
+ match result {
+ Ok(_) => {
+ let mut state = state.write().unwrap();
+ *state = MaterialState::Loaded;
+ }
+ Err(e) => {
+ tracing::error!("Failed to load material: {e}");
+ }
+ }
+ }
+ }
+
+ fn render_materials(&self, f: F)
+ where
+ F: Fn(RwLockReadGuard<'_, dyn Material>, RwLockReadGuard<'_, dyn Pipeline>),
+ {
+ let materials = self.materials.read().unwrap();
+ let materials_state = self.materials_state.read().unwrap();
+ let materials_pipeline = self.materials_pipeline.read().unwrap();
+ let materials_pipeline_state = self.materials_pipeline_state.read().unwrap();
+
+ materials
+ .iter()
+ .zip(materials_state.iter())
+ .zip(materials_pipeline.iter())
+ .zip(materials_pipeline_state.iter())
+ .filter(|(((_, material_state), _), pipeline_state)| {
+ let material_state = material_state.read().unwrap();
+ let pipeline_state = pipeline_state.read().unwrap();
+ matches!(*material_state, MaterialState::Loaded)
+ && matches!(*pipeline_state, MaterialState::Loaded)
+ })
+ .for_each(|(((material, _), pipeline), _)| {
+ let material = material.read().unwrap();
+ let pipeline = pipeline.read().unwrap();
+
+ f(material, pipeline);
+ });
+ }
+}
diff --git a/src/core/render/mod.rs b/src/core/render/mod.rs
new file mode 100644
index 0000000..ec7ac54
--- /dev/null
+++ b/src/core/render/mod.rs
@@ -0,0 +1,5 @@
+pub mod material_manager;
+pub mod primitives;
+pub mod render_pass_manager;
+pub mod texture;
+pub mod vulkan_context;
diff --git a/src/core/render/primitives/buffer.rs b/src/core/render/primitives/buffer.rs
new file mode 100644
index 0000000..e01226f
--- /dev/null
+++ b/src/core/render/primitives/buffer.rs
@@ -0,0 +1,83 @@
+use std::{error::Error, sync::Arc};
+
+use vulkano::{
+ Validated,
+ buffer::{AllocateBufferError, Buffer, BufferContents, BufferCreateInfo, Subbuffer},
+ memory::allocator::{AllocationCreateInfo, StandardMemoryAllocator},
+};
+
+pub trait AsBindableBuffer {
+ type BufferData: BufferContents + Clone;
+
+ fn buffer_create_info() -> BufferCreateInfo;
+
+ fn allocation_create_info() -> AllocationCreateInfo;
+
+ fn to_buffer_data(data: &T) -> Self::BufferData;
+
+ fn create_buffer(
+ memory_allocator: &Arc,
+ data: &T,
+ ) -> Result, Validated> {
+ let buffer_data = Self::to_buffer_data(data);
+ Buffer::from_iter(
+ memory_allocator.clone(),
+ Self::buffer_create_info(),
+ Self::allocation_create_info(),
+ [buffer_data],
+ )
+ }
+
+ fn update_buffer(
+ buffer: &Subbuffer<[Self::BufferData]>,
+ data: &T,
+ ) -> Result<(), Box> {
+ let buffer_data = Self::to_buffer_data(data);
+ let mut write_guard = buffer.write()?;
+ write_guard[0] = buffer_data;
+ Ok(())
+ }
+}
+
+pub trait AsUniformBuffer: AsBindableBuffer {
+ fn create_uniform_buffer(
+ memory_allocator: &Arc,
+ data: &T,
+ ) -> Result, Validated> {
+ Self::create_buffer(memory_allocator, data)
+ }
+}
+
+pub trait AsVertexBuffer: AsBindableBuffer {
+ fn create_vertex_buffer(
+ memory_allocator: &Arc,
+ vertices: &[T],
+ ) -> Result, Validated> {
+ let buffer_data: Vec =
+ vertices.iter().map(|v| Self::to_buffer_data(v)).collect();
+
+ Buffer::from_iter(
+ memory_allocator.clone(),
+ Self::buffer_create_info(),
+ Self::allocation_create_info(),
+ buffer_data,
+ )
+ }
+}
+
+pub trait AsIndexBuffer: AsBindableBuffer {
+ fn create_index_buffer(
+ memory_allocator: &Arc,
+ indices: &[T],
+ ) -> Result, Validated> {
+ let buffer_data: Vec =
+ indices.iter().map(|i| Self::to_buffer_data(i)).collect();
+
+ Buffer::from_iter(
+ memory_allocator.clone(),
+ Self::buffer_create_info(),
+ Self::allocation_create_info(),
+ buffer_data,
+ )
+ }
+}
diff --git a/src/core/render/primitives/camera.rs b/src/core/render/primitives/camera.rs
new file mode 100644
index 0000000..4fdf1ae
--- /dev/null
+++ b/src/core/render/primitives/camera.rs
@@ -0,0 +1,123 @@
+use std::{f32::consts::FRAC_PI_2, sync::Arc};
+
+use glam::{Mat4, Vec3, Vec4};
+use vulkano::{
+ Validated,
+ buffer::{AllocateBufferError, Subbuffer},
+ memory::allocator::StandardMemoryAllocator,
+};
+
+use crate::core::{input::InputManager, timer::Timer};
+
+use super::{AsUniformBuffer, mvp::Mvp};
+
+// See docs/OPENGL_VULKAN_DIFF.md
+const OPENGL_TO_VULKAN_Y_AXIS_FLIP: Mat4 = Mat4 {
+ x_axis: Vec4::new(1.0, 0.0, 0.0, 0.0),
+ y_axis: Vec4::new(0.0, -1.0, 0.0, 0.0),
+ z_axis: Vec4::new(0.0, 0.0, 1.0, 0.0),
+ w_axis: Vec4::new(0.0, 0.0, 0.0, 1.0),
+};
+
+#[derive(Default)]
+pub struct Camera3D {
+ projection: Mat4,
+
+ position: Vec3,
+ rotation: Vec3,
+ aspect_ratio: f32,
+ fov: f32,
+ near: f32,
+ far: f32,
+}
+
+impl Camera3D {
+ pub fn new(aspect_ratio: f32, fov: f32, near: f32, far: f32) -> Self {
+ Self {
+ projection: Mat4::perspective_rh(fov, aspect_ratio, near, far),
+ position: Vec3::ZERO,
+ rotation: Vec3::ZERO,
+ aspect_ratio,
+ fov,
+ near,
+ far,
+ }
+ }
+
+ pub fn update(
+ &mut self,
+ input_manager: &InputManager,
+ timer: &Timer,
+ movement_speed: f32,
+ camera_sensitivity: f32,
+ window_aspect_ratio: f32,
+ ) {
+ // Process camera rotation
+ let camera_delta = camera_sensitivity * timer.delta_time();
+ self.rotation += Vec3::new(
+ (input_manager.get_virtual_input_state("mouse_y") * camera_delta).to_radians(),
+ (input_manager.get_virtual_input_state("mouse_x") * camera_delta).to_radians(),
+ 0.0,
+ );
+
+ if self.rotation.x > FRAC_PI_2 {
+ self.rotation = Vec3::new(FRAC_PI_2, self.rotation.y, 0.0);
+ }
+
+ if self.rotation.x < -FRAC_PI_2 {
+ self.rotation = Vec3::new(-FRAC_PI_2, self.rotation.y, 0.0);
+ }
+
+ let movement_delta = movement_speed * timer.delta_time();
+
+ let (yaw_sin, yaw_cos) = self.rotation.y.sin_cos();
+ let forward = Vec3::new(yaw_cos, 0.0, yaw_sin).normalize();
+ let right = Vec3::new(-yaw_sin, 0.0, yaw_cos).normalize();
+
+ let tx = input_manager.get_virtual_input_state("move_right") * movement_delta;
+ self.position += tx * right;
+
+ let tz = input_manager.get_virtual_input_state("move_forward") * movement_delta;
+ self.position += tz * forward;
+
+ if self.aspect_ratio != window_aspect_ratio {
+ self.aspect_ratio = window_aspect_ratio;
+ self.projection =
+ Mat4::perspective_rh(self.fov, self.aspect_ratio, self.near, self.far);
+ }
+ }
+
+ pub fn set_projection(&mut self, projection: Mat4) {
+ self.projection = projection;
+ }
+
+ pub fn get_position(&self) -> Vec3 {
+ self.position
+ }
+
+ pub fn get_rotation(&self) -> Vec3 {
+ self.rotation
+ }
+
+ pub fn create_buffer(
+ &self,
+ memory_allocator: &Arc,
+ ) -> Result, Validated> {
+ let (sin_pitch, cos_pitch) = self.rotation.x.sin_cos();
+ let (sin_yaw, cos_yaw) = self.rotation.y.sin_cos();
+
+ let view_matrix = Mat4::look_to_rh(
+ self.position,
+ Vec3::new(cos_pitch * cos_yaw, sin_pitch, cos_pitch * sin_yaw).normalize(),
+ Vec3::Y,
+ );
+
+ let mvp = Mvp {
+ model: OPENGL_TO_VULKAN_Y_AXIS_FLIP.to_cols_array_2d(),
+ view: view_matrix.to_cols_array_2d(),
+ projection: self.projection.to_cols_array_2d(),
+ };
+
+ Mvp::create_uniform_buffer(memory_allocator, &mvp)
+ }
+}
diff --git a/src/core/render/primitives/command.rs b/src/core/render/primitives/command.rs
new file mode 100644
index 0000000..37970a1
--- /dev/null
+++ b/src/core/render/primitives/command.rs
@@ -0,0 +1,110 @@
+use std::{error::Error, sync::Arc};
+
+use vulkano::{
+ command_buffer::{
+ AutoCommandBufferBuilder, PrimaryAutoCommandBuffer, RenderPassBeginInfo, SubpassBeginInfo,
+ SubpassEndInfo,
+ },
+ descriptor_set::DescriptorSet,
+ pipeline::{GraphicsPipeline, Pipeline},
+ render_pass::{Framebuffer, RenderPass},
+};
+
+pub trait AsRecordable {
+ fn record_render_commands(
+ builder: &mut AutoCommandBufferBuilder,
+ data: &T,
+ pipeline: &Arc,
+ descriptor_sets: &[Arc],
+ ) -> Result<(), Box>;
+}
+
+pub trait AsDrawable {
+ type VertexBuffer;
+ type IndexBuffer;
+
+ fn vertex_buffer(data: &T) -> &Self::VertexBuffer;
+
+ fn index_buffer(_data: &T) -> Option<&Self::IndexBuffer> {
+ None
+ }
+
+ fn vertex_count(data: &T) -> u32;
+
+ fn index_count(_data: &T) -> u32 {
+ 0
+ }
+
+ fn instance_count(_data: &T) -> u32 {
+ 1
+ }
+
+ fn first_vertex(_data: &T) -> u32 {
+ 0
+ }
+
+ fn first_index(_data: &T) -> u32 {
+ 0
+ }
+
+ fn first_instance(_data: &T) -> u32 {
+ 0
+ }
+}
+
+pub trait AsRenderPassRecordable {
+ fn begin_render_pass(
+ builder: &mut AutoCommandBufferBuilder,
+ _render_pass: &Arc,
+ framebuffer: &Arc,
+ clear_values: Vec