diff options
Diffstat (limited to 'engine/src/rendering')
| -rw-r--r-- | engine/src/rendering/backend.rs | 9 | ||||
| -rw-r--r-- | engine/src/rendering/backend/opengl.rs | 2060 | ||||
| -rw-r--r-- | engine/src/rendering/backend/opengl/glutin_compat.rs | 264 | ||||
| -rw-r--r-- | engine/src/rendering/backend/opengl/graphics_mesh.rs | 259 | ||||
| -rw-r--r-- | engine/src/rendering/blending.rs | 89 | ||||
| -rw-r--r-- | engine/src/rendering/main_render_pass.rs | 1006 | ||||
| -rw-r--r-- | engine/src/rendering/object.rs | 137 | ||||
| -rw-r--r-- | engine/src/rendering/shader.rs | 1404 | ||||
| -rw-r--r-- | engine/src/rendering/shader/cursor.rs | 372 |
9 files changed, 5600 insertions, 0 deletions
diff --git a/engine/src/rendering/backend.rs b/engine/src/rendering/backend.rs new file mode 100644 index 0000000..d69b338 --- /dev/null +++ b/engine/src/rendering/backend.rs @@ -0,0 +1,9 @@ +use ecs::extension::Extension; + +pub mod opengl; + +/// Returns the default rendering backend. +pub fn get_default() -> impl Extension +{ + self::opengl::Extension::default() +} diff --git a/engine/src/rendering/backend/opengl.rs b/engine/src/rendering/backend/opengl.rs new file mode 100644 index 0000000..5b009ca --- /dev/null +++ b/engine/src/rendering/backend/opengl.rs @@ -0,0 +1,2060 @@ +//! OpenGL rendering backend. + +use std::borrow::Cow; +use std::collections::HashMap; +use std::convert::Infallible; +use std::hint::cold_path; +use std::num::NonZero; + +use ecs::query::term::With; +use glutin::config::Config as GlutinConfig; +use glutin::display::GetGlDisplay; +use glutin::error::Error as GlutinError; +use glutin::prelude::{GlDisplay, PossiblyCurrentGlContext}; +use glutin::surface::{ + GlSurface as _, + Surface as GlutinSurface, + WindowSurface as GlutinWindowSurface, +}; +use intmap::IntMap; +use opengl_bindings::blending::{ + configure as gl_blending_configure, + Configuration as GlBlendingConfig, + Equation as GlBlendingEquation, + Factor as GlBlendingFactor, +}; +use opengl_bindings::buffer::{ + BindingTarget as GlBufferBindingTarget, + Buffer as GlBuffer, +}; +use opengl_bindings::debug::{ + set_debug_message_callback, + set_debug_message_control, + MessageIdsAction, + MessageSeverity, + MessageSource, + MessageType, + SetDebugMessageControlError as GlSetDebugMessageControlError, +}; +use opengl_bindings::framebuffer::{ + bind as gl_bind_framebuffer, + Attachment as GlFramebufferAttachment, + ColorAttachment as GlFramebufferColorAttachment, + Framebuffer as GlFramebuffer, + Target as GlFramebufferTarget, +}; +use opengl_bindings::misc::{ + clear_buffers, + define_scissor_box as gl_define_scissor_box, + enable, + get_viewport as gl_get_viewport, + set_depth_function as gl_set_depth_function, + set_enabled, + set_face_culling_mode as gl_set_face_culling_mode, + set_viewport as gl_set_viewport, + BufferClearMask as GlBufferClearMask, + Capability, + DepthFunction as GlDepthFunction, + FaceCullingMode as GlFaceCullingMode, + PolygonMode as GlPolygonMode, + PolygonModeFace as GlPolygonModeFace, +}; +use opengl_bindings::shader::{ + Error as GlShaderError, + Kind as ShaderKind, + Program as GlShaderProgram, + Shader as GlShader, +}; +use opengl_bindings::texture::{ + CubeMapFace as GlCubeMapTextureFace, + Error as GlTextureError, + Filtering as GlTextureFiltering, + PixelDataFormat as GlTexturePixelDataFormat, + Texture as GlTexture, + Wrapping as GlTextureWrapping, +}; +use opengl_bindings::vertex_array::{ + DrawError as GlDrawError, + PrimitiveKind, + VertexArray, +}; +use opengl_bindings::{ + MakeContextCurrentError as GlMakeContextCurrentError, + MaybeCurrentContextWithFns, +}; +use raw_window_handle::WindowHandle; +use safer_ffi::layout::ReprC; +use zerocopy::IntoBytes; + +use crate::asset::Assets; +use crate::data_types::color::Color; +use crate::data_types::dimens::Dimens; +use crate::draw_flags::{PolygonMode, PolygonModeFace}; +use crate::ecs::actions::Actions; +use crate::ecs::query::term::Without; +use crate::ecs::sole::Single; +use crate::ecs::{Component, Query, Sole}; +use crate::reflection::EnumReflectionExt; +use crate::rendering::backend::opengl::glutin_compat::{ + DisplayBuilder, + Error as GlutinCompatError, +}; +use crate::rendering::backend::opengl::graphics_mesh::GraphicsMesh; +use crate::rendering::blending::{ + Equation as BlendingEquation, + Factor as BlendingFactor, +}; +use crate::rendering::object::{ + Id as ObjectId, + Kind as ObjectKind, + Object, + RawValue as ObjectRawValue, + Store as ObjectStore, +}; +use crate::rendering::shader::cursor::{ + Binding as ShaderBinding, + BindingLocation as ShaderBindingLocation, + BindingValue as ShaderBindingValue, +}; +use crate::rendering::shader::{ + Error as ShaderError, + Program as ShaderProgram, + ProgramMetadata as ShaderProgramMetadata, + Stage as ShaderStage, +}; +use crate::rendering::{ + AssetOrValue, + BufferClearMask, + Command, + CommandQueue, + DepthFunction, + DrawMeshOptions, + DrawProperties, + DrawPropertiesUpdateFlags, + FaceCullingMode, + FramebufferProperties, + FramebufferPropertiesUpdateFlags, + GraphicsProperties, + Surface, + SurfaceId, + TargetWindow, + TextureCreation, + TexturePixelDataFormat, + TextureUpdate, + POST_RENDER_PHASE, + RENDER_PHASE, +}; +use crate::texture::{ + Border as TextureBorder, + CubeMapFace as CubeMapTextureFace, + Filtering as TextureFiltering, + Properties as TextureProperties, + Wrapping as TextureWrapping, +}; +use crate::util::OptionExt; +use crate::vector::{Vec2, Vec3}; +use crate::windowing::dpi::PhysicalSize; +use crate::windowing::window::{ + Closed as WindowClosed, + CreationAttributes as WindowCreationAttributes, + CreationReady, + Window, +}; +use crate::windowing::Context as WindowingContext; + +mod glutin_compat; +mod graphics_mesh; + +#[derive(Debug, Component)] +struct WindowGlConfig +{ + gl_config: GlutinConfig, +} + +#[derive(Sole, Default)] +struct GraphicsContext +{ + gl_context: Option<MaybeCurrentContextWithFns>, + surfaces: HashMap<SurfaceId, GraphicsContextSurface>, + backend_resources: BackendResourceStore, +} + +#[derive(Debug, Default)] +struct BackendResourceStore +{ + inner: IntMap<BackendResourceId, BackendResource>, + next_id: BackendResourceId, +} + +impl BackendResourceStore +{ + fn try_create_resource<Err>( + &mut self, + object_store: &mut ObjectStore, + object_id: ObjectId, + resource_fn: impl FnOnce() -> Result<BackendResource, Err>, + ) -> Result<(), Err> + { + if object_store.contains_non_pending_with_id(&object_id) { + cold_path(); + tracing::error!( + ?object_id, + "Object store already contains a object with this ID" + ); + return Ok(()); + } + + let resource = resource_fn()?; + + let res_id = self.next_id; + + self.next_id.0 += 1; + + let object_kind = resource.object_kind(); + + self.inner.insert(res_id, resource); + + object_store.insert(object_id, Object::from_raw(res_id.0, object_kind)); + + Ok(()) + } + + fn get_resource( + &self, + object_store: &ObjectStore, + object_id: ObjectId, + object_kind: ObjectKind, + ) -> Option<&BackendResource> + { + let Some(object) = object_store.get_obj(&object_id) else { + cold_path(); + tracing::error!( + ?object_id, + %object_kind, + "Object does not exist in the rendering object store" + ); + return None; + }; + + if object.kind() != object_kind { + cold_path(); + tracing::error!( + ?object_id, + expected_object_kind = %object_kind, + found_object_kind = %object.kind(), + "Unexpected object kind of object in object store" + ); + return None; + } + + let resource_id = BackendResourceId(object.as_raw()); + + let Some(resource) = self.inner.get(resource_id) else { + cold_path(); + tracing::error!( + ?object_id, + %object_kind, + ?resource_id, + "Backend resource does not exist" + ); + return None; + }; + + debug_assert_eq!(resource.object_kind(), object_kind); + + Some(resource) + } + + fn get_resource_mut( + &mut self, + object_store: &ObjectStore, + object_id: ObjectId, + object_kind: ObjectKind, + ) -> Option<&mut BackendResource> + { + let Some(object) = object_store.get_obj(&object_id) else { + cold_path(); + tracing::error!( + ?object_id, + %object_kind, + "Object does not exist in the rendering object store" + ); + return None; + }; + + if object.kind() != object_kind { + cold_path(); + tracing::error!( + ?object_id, + expected_object_kind = %object_kind, + found_object_kind = %object.kind(), + "Unexpected object kind of object in object store" + ); + return None; + } + + let resource_id = BackendResourceId(object.as_raw()); + + let Some(resource) = self.inner.get_mut(resource_id) else { + cold_path(); + tracing::error!( + ?object_id, + %object_kind, + ?resource_id, + "Backend resource does not exist" + ); + return None; + }; + + debug_assert_eq!(resource.object_kind(), object_kind); + + Some(resource) + } + + fn remove_resource( + &mut self, + object_store: &mut ObjectStore, + object_id: ObjectId, + object_kind: ObjectKind, + curr_gl_ctx: &MaybeCurrentContextWithFns, + ) + { + if let Some(object) = object_store.get_obj(&object_id) { + if object.kind() != object_kind { + cold_path(); + tracing::error!( + ?object_id, + expected_object_kind = %object_kind, + found_object_kind = %object.kind(), + "Unexpected object kind" + ); + return; + } + } + + let Some(object) = object_store.remove(&object_id).flatten() else { + tracing::error!( + ?object_id, + %object_kind, + "Object does not exist in the rendering object store" + ); + return; + }; + + let resource_id = BackendResourceId(object.as_raw()); + + let Some(resource) = self.inner.remove(resource_id) else { + cold_path(); + tracing::error!( + ?object_id, + %object_kind, + ?resource_id, + "Backend resource does not exist" + ); + return; + }; + + resource.destroy(curr_gl_ctx); + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct BackendResourceId(ObjectRawValue); + +impl intmap::IntKey for BackendResourceId +{ + type Int = ObjectRawValue; + + const PRIME: Self::Int = <ObjectRawValue as intmap::IntKey>::PRIME; + + fn into_int(self) -> Self::Int + { + self.0.into_int() + } +} + +#[derive(Debug)] +struct GraphicsContextSurface +{ + window_surface: GlutinSurface<GlutinWindowSurface>, + size: PhysicalSize<u32>, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct ShaderBindingIndex(u32); + +impl intmap::IntKey for ShaderBindingIndex +{ + type Int = u32; + + const PRIME: Self::Int = <u32 as intmap::IntKey>::PRIME; + + fn into_int(self) -> Self::Int + { + self.0 + } +} + +#[derive(Debug)] +enum BackendShaderBinding +{ + Uniform(GlBuffer<u8>), + Texture(GlTexture), +} + +impl BackendShaderBinding +{ + fn bind( + &self, + gl_context: &MaybeCurrentContextWithFns, + binding_index: ShaderBindingIndex, + ) + { + match self { + BackendShaderBinding::Uniform(uniform_buf) => { + uniform_buf.bind_to_indexed_target( + gl_context, + GlBufferBindingTarget::UniformBuffer, + binding_index.0, + ); + } + BackendShaderBinding::Texture(texture) => { + texture.bind_to_texture_unit(gl_context, binding_index.0) + } + } + } +} + +#[derive(Debug)] +enum BackendResource +{ + Mesh + { + mesh: GraphicsMesh, + vertex_attrs_updated_for_shader: Option<ObjectId>, + }, + Shader + { + program: GlShaderProgram, + program_metadata: ShaderProgramMetadata, + bindings: IntMap<ShaderBindingIndex, BackendShaderBinding>, + }, + Texture + { + texture: GlTexture, + pixel_data_format: TexturePixelDataFormat, + kind: TextureKind, + }, + Framebuffer + { + framebuffer: GlFramebuffer + }, +} + +impl BackendResource +{ + fn object_kind(&self) -> ObjectKind + { + match self { + Self::Mesh { .. } => ObjectKind::Mesh, + Self::Shader { .. } => ObjectKind::ShaderProgram, + Self::Texture { .. } => ObjectKind::Texture, + Self::Framebuffer { .. } => ObjectKind::Framebuffer, + } + } + + fn destroy(&self, curr_gl_ctx: &MaybeCurrentContextWithFns) + { + match self { + Self::Mesh { mesh, .. } => { + mesh.destroy(curr_gl_ctx); + } + Self::Shader { program, .. } => { + program.clone().delete(curr_gl_ctx); + } + Self::Texture { texture, .. } => { + texture.clone().delete(curr_gl_ctx); + } + Self::Framebuffer { framebuffer } => { + framebuffer.clone().delete(curr_gl_ctx); + } + } + } +} + +#[derive(Debug)] +enum TextureKind +{ + Texture2D, + CubeMap, +} + +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct Extension {} + +impl crate::ecs::extension::Extension for Extension +{ + fn collect(self, mut collector: crate::ecs::extension::Collector<'_>) + { + collector.add_system(*RENDER_PHASE, handle_commands); + + collector.add_system(*POST_RENDER_PHASE, prepare_windows); + collector.add_system(*POST_RENDER_PHASE, init_window_graphics); + + let _ = collector.add_sole(GraphicsContext::default()); + } +} + +fn prepare_windows( + window_query: Query<( + Option<&Window>, + &mut WindowCreationAttributes, + With<TargetWindow>, + Without<CreationReady>, + Without<WindowGlConfig>, + Without<WindowClosed>, + )>, + windowing_context: Single<WindowingContext>, + graphics_props: Single<GraphicsProperties>, + mut actions: Actions, +) -> Result<(), crate::Error> +{ + let windowing_context = windowing_context.get()?; + let graphics_props = graphics_props.get()?; + + let Some(display_handle) = windowing_context.display_handle() else { + return Ok(()); + }; + + for (window_ent_id, (window, mut window_creation_attrs)) in + window_query.iter_with_euids() + { + tracing::debug!("Preparing window entity {window_ent_id} for use in rendering"); + + let mut glutin_config_template_builder = + glutin::config::ConfigTemplateBuilder::new(); + + if let Some(multisampling_sample_cnt) = graphics_props.multisampling_sample_cnt { + glutin_config_template_builder = glutin_config_template_builder + .with_multisampling(multisampling_sample_cnt); + } + + let window_handle = match window + .as_ref() + .map(|window| unsafe { + windowing_context.get_window_as_handle(&window.wid()) + }) + .flatten() + .transpose() + { + Ok(window_handle) => window_handle, + Err(err) => { + tracing::error!("Failed to get window handle: {err}"); + continue; + } + }; + + let (new_window_creation_attrs, gl_config) = match DisplayBuilder::new() + .with_window_attributes(window_creation_attrs.clone()) + .build( + window_handle, + &display_handle, + glutin_config_template_builder, + |mut cfgs| cfgs.next(), + ) { + Ok((new_window_creation_attrs, gl_config)) => { + (new_window_creation_attrs, gl_config) + } + Err(GlutinCompatError::WindowRequired) => { + actions.add_components(window_ent_id, (CreationReady,)); + continue; + } + Err(err) => { + tracing::error!("Failed to create platform graphics display: {err}"); + continue; + } + }; + + *window_creation_attrs = new_window_creation_attrs; + + actions.add_components(window_ent_id, (WindowGlConfig { gl_config },)); + + if window.is_none() { + actions.add_components(window_ent_id, (CreationReady,)); + } + } + + Ok(()) +} + +#[tracing::instrument(skip_all)] +fn init_window_graphics( + window_query: Query<( + &Window, + &WindowGlConfig, + With<TargetWindow>, + Without<Surface>, + )>, + windowing_context: Single<WindowingContext>, + graphics_props: Single<GraphicsProperties>, + mut graphics_ctx: Single<GraphicsContext>, + mut actions: Actions, +) -> Result<(), crate::Error> +{ + let Ok(graphics_ctx) = graphics_ctx.get_mut() else { + unreachable!(); + }; + + let windowing_context = windowing_context.get()?; + let graphics_props = graphics_props.get()?; + + for (window_ent_id, (window, window_gl_config)) in window_query.iter_with_euids() { + tracing::info!( + window_entity_id=%window_ent_id, + window_title=&*window.title, + "Initializing graphics for window" + ); + + let display = window_gl_config.gl_config.display(); + + let window_handle = + match unsafe { windowing_context.get_window_as_handle(&window.wid()) } + .transpose() + { + Ok(Some(window_handle)) => window_handle, + Ok(None) => { + tracing::error!( + wid = ?window.wid(), + entity_id = %window_ent_id, + "Windowing context does not contain window" + ); + continue; + } + Err(err) => { + tracing::error!("Failed to get window handle: {err}"); + continue; + } + }; + + let Ok(window_inner_size) = + PhysicalSize::<NonZero<u32>>::try_convert_from(window.inner_size.clone()) + else { + tracing::error!( + "Cannot create a surface for a window with a width/height of 0", + ); + continue; + }; + + let window_surface = match unsafe { + display.create_window_surface( + &window_gl_config.gl_config, + &glutin::surface::SurfaceAttributesBuilder::< + glutin::surface::WindowSurface, + >::new() + .build( + window_handle.as_raw(), + window_inner_size.width, + window_inner_size.height, + ), + ) + } { + Ok(window_surface) => window_surface, + Err(err) => { + tracing::error!("Failed to create window surface: {err}"); + continue; + } + }; + + let gl_context = match graphics_ctx.gl_context.get_or_try_insert_with_fn(|| { + create_gl_context( + &window_gl_config.gl_config, + &graphics_props, + window_handle, + &window_surface, + ) + }) { + Ok(gl_context) => gl_context, + Err(err) => { + tracing::error!("Failed to create GL context: {err}"); + continue; + } + }; + + if let Err(err) = gl_context.make_current(&window_surface) { + tracing::error!("Failed to make GL context current: {err}"); + continue; + }; + + if let Err(err) = gl_set_viewport( + &gl_context, + &Vec2 { x: 0, y: 0 }.into(), + &opengl_bindings::data_types::Dimens { + width: window.inner_size.width, + height: window.inner_size.height, + }, + ) { + tracing::error!("Failed to set viewport: {err}"); + } + + set_enabled( + &gl_context, + Capability::DepthTest, + graphics_props.depth_test, + ); + + set_enabled( + &gl_context, + Capability::MultiSample, + graphics_props.multisampling_sample_cnt.is_some(), + ); + + if graphics_props.debug { + enable(&gl_context, Capability::DebugOutput); + enable(&gl_context, Capability::DebugOutputSynchronous); + + set_debug_message_callback(&gl_context, opengl_debug_message_cb); + + match set_debug_message_control( + &gl_context, + None, + None, + None, + &[], + MessageIdsAction::Disable, + ) { + Ok(()) => {} + Err(GlSetDebugMessageControlError::TooManyIds { + id_cnt: _, + max_id_cnt: _, + }) => { + unreachable!() // No ids are given + } + } + } + + let surface_id = SurfaceId::new_unique(); + + actions.add_components( + window_ent_id, + (Surface { + id: surface_id, + size: window.inner_size.clone(), + },), + ); + + graphics_ctx.surfaces.insert( + surface_id, + GraphicsContextSurface { + window_surface, + size: window.inner_size.clone(), + }, + ); + } + + Ok(()) +} + +#[tracing::instrument(skip_all)] +fn handle_commands( + mut graphics_ctx: Single<GraphicsContext>, + mut object_store: Single<ObjectStore>, + mut command_queue: Single<CommandQueue>, + assets: Single<Assets>, +) -> Result<(), crate::Error> +{ + let Ok(graphics_ctx) = graphics_ctx.get_mut() else { + unreachable!(); + }; + + let object_store = object_store.get_mut()?; + let command_queue = command_queue.get_mut()?; + let assets = assets.get()?; + + let GraphicsContext { + ref gl_context, + ref mut surfaces, + ref mut backend_resources, + } = *graphics_ctx; + + let Some(gl_context) = gl_context else { + return Ok(()); + }; + + let mut activated_gl_shader_program: Option<( + ObjectId, + GlShaderProgram, + ShaderProgramMetadata, + )> = None; + + for command in command_queue.drain() { + let tracing_span = tracing::info_span!( + "handle_cmd", + command = %command.get_variant_reflection().name, + ); + let _tracing_span_enter = tracing_span.enter(); + + match command { + Command::RemoveSurface(surface_id) => { + let Some(surface) = surfaces.remove(&surface_id) else { + tracing::error!(surface_id=?surface_id, "Surface does not exist"); + continue; + }; + + if surface.window_surface.is_current(gl_context.context()) { + if let Err(err) = gl_context.context().make_not_current_in_place() { + tracing::error!("Failed to make GL context not current: {err}"); + } + + if let Err(err) = gl_context.make_current_surfaceless() { + tracing::error!("Failed to make GL context current: {err}"); + } + } + + drop(surface); + } + Command::MakeCurrent(surface_id) => { + let Some(surface) = surfaces.get(&surface_id) else { + tracing::error!(surface_id=?surface_id, "Surface does not exist"); + continue; + }; + + if let Err(err) = gl_context.make_current(&surface.window_surface) { + tracing::error!("Failed to make graphics context current: {err}"); + continue; + } + + if let Err(err) = gl_set_viewport( + gl_context, + &Vec2 { x: 0, y: 0 }.into(), + &opengl_bindings::data_types::Dimens { + width: surface.size.width, + height: surface.size.height, + }, + ) { + tracing::error!("Failed to set viewport: {err}"); + } + } + Command::SetSurfaceSize(surface_id, new_surface_size) => { + let Some(surface) = surfaces.get_mut(&surface_id) else { + tracing::error!(surface_id=?surface_id, "Surface does not exist"); + continue; + }; + + surface.size = new_surface_size; + + if !surface.window_surface.is_current(gl_context.context()) { + continue; + } + + if let Err(err) = gl_set_viewport( + gl_context, + &Vec2 { x: 0, y: 0 }.into(), + &opengl_bindings::data_types::Dimens { + width: surface.size.width, + height: surface.size.height, + }, + ) { + tracing::error!("Failed to set viewport: {err}"); + } + } + Command::ClearBuffers(buffer_clear_mask) => { + let mut clear_mask = GlBufferClearMask::empty(); + + clear_mask.set( + GlBufferClearMask::COLOR, + buffer_clear_mask.contains(BufferClearMask::COLOR), + ); + + clear_mask.set( + GlBufferClearMask::DEPTH, + buffer_clear_mask.contains(BufferClearMask::DEPTH), + ); + + clear_mask.set( + GlBufferClearMask::STENCIL, + buffer_clear_mask.contains(BufferClearMask::STENCIL), + ); + + clear_buffers(gl_context, clear_mask); + } + Command::SwapBuffers(surface_id) => { + let Some(surface) = surfaces.get(&surface_id) else { + tracing::error!(surface_id=?surface_id, "Surface does not exist"); + continue; + }; + + if let Err(err) = + surface.window_surface.swap_buffers(gl_context.context()) + { + tracing::error!("Failed to swap buffers: {err}"); + } + } + Command::CreateShaderProgram(object_id, shader_program) => { + if let Err(err) = backend_resources + .try_create_resource::<CreateShaderError>( + object_store, + object_id, + || { + Ok(BackendResource::Shader { + program: create_shader_program( + gl_context, + &shader_program, + )?, + program_metadata: shader_program.metadata().clone(), + bindings: IntMap::with_capacity(4), + }) + }, + ) + { + tracing::error!("Failed to create shader program: {err}"); + } + } + Command::ActivateShader(shader_object_id) => { + let Some(resource) = backend_resources.get_resource( + object_store, + shader_object_id, + ObjectKind::ShaderProgram, + ) else { + continue; + }; + + let BackendResource::Shader { program, program_metadata, bindings } = + resource + else { + unreachable!(); + }; + + program.activate(gl_context); + + for (binding_index, binding) in bindings.iter() { + binding.bind(gl_context, binding_index); + } + + activated_gl_shader_program = + Some((shader_object_id, program.clone(), program_metadata.clone())); + } + Command::SetShaderBinding( + shader_object_id, + ShaderBinding { + location: binding_location, + value: binding_value, + }, + ) => { + let binding = set_shader_binding( + backend_resources, + object_store, + gl_context, + shader_object_id.clone(), + &binding_location, + binding_value, + ); + + if let Some(binding) = binding { + if Some(shader_object_id) + == activated_gl_shader_program + .as_ref() + .map(|(obj_id, ..)| *obj_id) + { + binding.bind( + gl_context, + ShaderBindingIndex(binding_location.binding_index), + ); + } + } + } + Command::CreateTexture { + obj_id, + pixel_data_format, + creation, + properties, + } => { + let kind = match &creation { + TextureCreation::Texture2D { .. } => TextureKind::Texture2D, + TextureCreation::CubeMap { .. } => TextureKind::CubeMap, + }; + + if let Err(err) = backend_resources + .try_create_resource::<opengl_bindings::texture::Error>( + object_store, + obj_id, + || { + let gl_texture = create_gl_texture( + gl_context, + creation, + pixel_data_format, + &properties, + )?; + + Ok(BackendResource::Texture { + texture: gl_texture, + pixel_data_format, + kind, + }) + }, + ) + { + tracing::error!("Failed to create texture: {err}"); + } + } + Command::UpdateTexture { + obj_id, + pixels, + pixel_data_format, + update, + } => { + let Some(texture_resource) = backend_resources.get_resource( + object_store, + obj_id, + ObjectKind::Texture, + ) else { + continue; + }; + + let BackendResource::Texture { + texture: gl_texture, + pixel_data_format: tex_pixel_data_format, + kind: tex_metadata, + } = texture_resource + else { + unreachable!(); + }; + + if !matches!( + (&update, tex_metadata), + (TextureUpdate::Texture2D { .. }, TextureKind::Texture2D) + | (TextureUpdate::CubeMap { .. }, TextureKind::CubeMap) + ) { + tracing::error!( + texture_object_id = ?obj_id, + "Texture is of incorrect kind" + ); + continue; + } + + if &pixel_data_format != tex_pixel_data_format { + cold_path(); + tracing::error!( + texture_object_id = ?obj_id, + expected_pixel_data_format = ?*tex_pixel_data_format, + found_pixel_data_format = ?pixel_data_format, + "Texture has incorrect pixel data format" + ); + continue; + } + + if let Err(err) = update_texture_object( + gl_context, + gl_texture, + pixels, + pixel_data_format, + update, + ) { + tracing::error!("Failed to update texture object: {err}"); + } + } + Command::RemoveTexture(object_id) => { + backend_resources.remove_resource( + object_store, + object_id, + ObjectKind::Texture, + gl_context, + ); + } + Command::CreateFramebuffer(object_id) => { + backend_resources.try_create_resource::<Infallible>( + object_store, + object_id, + || { + Ok(BackendResource::Framebuffer { + framebuffer: GlFramebuffer::new(gl_context), + }) + }, + ); + } + Command::RemoveFramebuffer(object_id) => { + backend_resources.remove_resource( + object_store, + object_id, + ObjectKind::Framebuffer, + gl_context, + ); + } + Command::UpdateFramebufferProperties { + object_id, + properties, + update_flags, + } => { + update_framebuffer_properties( + backend_resources, + object_store, + gl_context, + object_id, + properties, + update_flags, + ); + } + Command::CreateMesh { obj_id, mesh, usage: mesh_usage } => { + let mesh = match &mesh { + AssetOrValue::Asset(mesh_asset) => { + let Some(mesh) = assets.get(&mesh_asset) else { + tracing::error!( + asset_id=?mesh_asset.id(), + "Mesh asset does not exist" + ); + continue; + }; + + mesh + } + AssetOrValue::Value(mesh) => mesh, + }; + + if let Err(err) = backend_resources + .try_create_resource::<graphics_mesh::Error>( + object_store, + obj_id, + || { + Ok(BackendResource::Mesh { + mesh: GraphicsMesh::new(gl_context, &mesh, mesh_usage)?, + vertex_attrs_updated_for_shader: None, + }) + }, + ) + { + tracing::error!("Failed to create mesh: {err}"); + } + } + Command::UpdateMesh { obj_id, mesh, usage: mesh_usage } => { + let Some(resource) = backend_resources.get_resource_mut( + object_store, + obj_id, + ObjectKind::Mesh, + ) else { + continue; + }; + + let BackendResource::Mesh { mesh: graphics_mesh, .. } = resource else { + unreachable!(); + }; + + if let Err(err) = graphics_mesh.update(gl_context, &mesh, mesh_usage) { + tracing::error!("Failed to update mesh: {err}"); + } + } + Command::RemoveMesh(object_id) => { + backend_resources.remove_resource( + object_store, + object_id, + ObjectKind::Mesh, + gl_context, + ); + } + Command::DrawMesh(object_id, draw_mesh_opts) => { + let Some(resource) = backend_resources.get_resource_mut( + object_store, + object_id, + ObjectKind::Mesh, + ) else { + continue; + }; + + let BackendResource::Mesh { + mesh: graphics_mesh, + vertex_attrs_updated_for_shader, + } = resource + else { + unreachable!(); + }; + + let Some((shader_program_obj_id, _, shader_program_metadata)) = + &activated_gl_shader_program + else { + tracing::error!("No shader program is activated"); + continue; + }; + + if vertex_attrs_updated_for_shader.is_none_or( + |vertex_attrs_updated_for_shader| { + vertex_attrs_updated_for_shader != *shader_program_obj_id + }, + ) { + let Some(shader_program_vertex_desc) = + &shader_program_metadata.vertex_desc + else { + tracing::error!( + "Activated shader program does not have a vertex description" + ); + continue; + }; + + if let Err(err) = graphics_mesh.update_vertex_attrs_for_shader( + gl_context, + shader_program_vertex_desc, + ) { + tracing::error!("Cannot draw mesh: {err}"); + continue; + } + + *vertex_attrs_updated_for_shader = Some(*shader_program_obj_id); + } + + if let Err(err) = draw_mesh(gl_context, graphics_mesh, &draw_mesh_opts) { + tracing::error!("Failed to draw mesh: {err}"); + }; + } + Command::UpdateDrawProperties(properties, update_flags) => { + update_draw_properties( + backend_resources, + object_store, + gl_context, + properties, + update_flags, + ); + } + } + } + + Ok(()) +} + +fn create_gl_context( + gl_config: &GlutinConfig, + graphics_props: &GraphicsProperties, + window_handle: WindowHandle<'_>, + surface: &GlutinSurface<GlutinWindowSurface>, +) -> Result<MaybeCurrentContextWithFns, CreateGlContextError> +{ + let display = gl_config.display(); + + let glutin_context = unsafe { + display.create_context( + gl_config, + &glutin::context::ContextAttributesBuilder::new() + .with_debug(graphics_props.debug) + .build(Some(window_handle.as_raw())), + ) + } + .map_err(CreateGlContextError::CreateGlutinContext)?; + + MaybeCurrentContextWithFns::new(glutin_context, &surface) + .map_err(CreateGlContextError::MakeContextCurrent) +} + +#[derive(Debug, thiserror::Error)] +enum CreateGlContextError +{ + #[error("Glutin context creation failed")] + CreateGlutinContext(#[source] GlutinError), + + #[error("Making GL context current failed")] + MakeContextCurrent(#[source] GlMakeContextCurrentError), +} + +#[tracing::instrument(skip_all)] +fn update_texture_object( + curr_gl_ctx: &MaybeCurrentContextWithFns, + gl_texture: &GlTexture, + pixels: Box<[u8]>, + pixel_data_format: TexturePixelDataFormat, + update: TextureUpdate, +) -> Result<(), GlTextureError> +{ + match update { + TextureUpdate::Texture2D { size, offset } => { + gl_texture.store_image_2d( + curr_gl_ctx, + 0, + offset.into(), + size.into(), + tex_pixel_data_format_into_gl(pixel_data_format), + &pixels, + )?; + } + TextureUpdate::CubeMap { size, offset, face } => { + gl_texture.store_image_3d( + curr_gl_ctx, + 0, + Vec3 { + x: offset.x, + y: offset.y, + z: cube_map_texture_face_to_gl(face) as u32, + } + .into(), + [size.width, size.height, 1], + tex_pixel_data_format_into_gl(pixel_data_format), + &pixels, + )?; + } + } + + gl_texture.generate_mipmap(curr_gl_ctx); + + Ok(()) +} + +#[tracing::instrument(skip_all)] +fn update_draw_properties( + backend_resources: &BackendResourceStore, + object_store: &ObjectStore, + gl_context: &MaybeCurrentContextWithFns, + properties: DrawProperties, + update_flags: DrawPropertiesUpdateFlags, +) +{ + if update_flags.contains(DrawPropertiesUpdateFlags::POLYGON_MODE_CONFIG) { + opengl_bindings::misc::set_polygon_mode( + gl_context, + polygon_mode_face_to_gl(properties.polygon_mode_config.face), + polygon_mode_to_gl(properties.polygon_mode_config.mode), + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::BLENDING_ENABLED) { + set_enabled(gl_context, Capability::Blend, properties.blending_enabled); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::BLENDING_CONFIG) { + gl_blending_configure( + gl_context, + GlBlendingConfig::default() + .with_source_factor(blending_factor_to_gl( + properties.blending_config.source_factor, + )) + .with_destination_factor(blending_factor_to_gl( + properties.blending_config.destination_factor, + )) + .with_equation(blending_equation_to_gl( + properties.blending_config.equation, + )), + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::DEPTH_TEST_ENABLED) { + set_enabled( + gl_context, + Capability::DepthTest, + properties.depth_test_enabled, + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::DEPTH_FUNCTION) { + gl_set_depth_function( + gl_context, + match properties.depth_function { + DepthFunction::Never => GlDepthFunction::Never, + DepthFunction::Less => GlDepthFunction::Less, + DepthFunction::Equal => GlDepthFunction::Equal, + DepthFunction::LessOrEqual => GlDepthFunction::LessOrEqual, + DepthFunction::Greater => GlDepthFunction::Greater, + DepthFunction::NotEqual => GlDepthFunction::NotEqual, + DepthFunction::GreaterOrEqual => GlDepthFunction::GreaterOrEqual, + DepthFunction::Always => GlDepthFunction::Always, + }, + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::SCISSOR_TEST_ENABLED) { + set_enabled( + gl_context, + Capability::ScissorTest, + properties.scissor_test_enabled, + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::SCISSOR_BOX) { + gl_define_scissor_box( + gl_context, + properties.scissor_box.lower_left_corner_pos.into(), + properties + .scissor_box + .size + .unwrap_or_else(|| { + let (_, viewport_size) = gl_get_viewport(gl_context); + + Dimens::<u16> { + width: viewport_size + .width + .try_into() + .expect("Viewport width too large"), + height: viewport_size + .height + .try_into() + .expect("Viewport height too large"), + } + }) + .into(), + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::FACE_CULLING_ENABLED) { + set_enabled( + gl_context, + Capability::CullFace, + properties.face_culling_enabled, + ); + } + + if update_flags.contains(DrawPropertiesUpdateFlags::FACE_CULLING_MODE) { + gl_set_face_culling_mode( + gl_context, + match properties.face_culling_mode { + FaceCullingMode::Front => GlFaceCullingMode::Front, + FaceCullingMode::Back => GlFaceCullingMode::Back, + FaceCullingMode::FrontAndBack => GlFaceCullingMode::FrontAndBack, + }, + ); + } + + 'if_draw_fb: { + if update_flags.contains(DrawPropertiesUpdateFlags::DRAW_FRAMEBUFFER) { + let Some(framebuffer_obj_id) = properties.draw_framebuffer else { + gl_bind_framebuffer(gl_context, GlFramebufferTarget::Draw, None); + break 'if_draw_fb; + }; + + let Some(framebuffer_resource) = backend_resources.get_resource( + object_store, + framebuffer_obj_id, + ObjectKind::Framebuffer, + ) else { + break 'if_draw_fb; + }; + + let BackendResource::Framebuffer { framebuffer } = framebuffer_resource + else { + unreachable!(); + }; + + gl_bind_framebuffer(gl_context, GlFramebufferTarget::Draw, Some(framebuffer)); + } + } + + 'if_read_fb: { + if update_flags.contains(DrawPropertiesUpdateFlags::READ_FRAMEBUFFER) { + let Some(framebuffer_obj_id) = properties.read_framebuffer else { + gl_bind_framebuffer(gl_context, GlFramebufferTarget::Read, None); + break 'if_read_fb; + }; + + let Some(framebuffer_resource) = backend_resources.get_resource( + object_store, + framebuffer_obj_id, + ObjectKind::Framebuffer, + ) else { + break 'if_read_fb; + }; + + let BackendResource::Framebuffer { framebuffer } = framebuffer_resource + else { + unreachable!(); + }; + + gl_bind_framebuffer(gl_context, GlFramebufferTarget::Read, Some(framebuffer)); + } + } +} + +#[tracing::instrument(skip_all)] +fn update_framebuffer_properties( + backend_resources: &BackendResourceStore, + object_store: &ObjectStore, + current_context: &MaybeCurrentContextWithFns, + framebuffer_object_id: ObjectId, + properties: FramebufferProperties, + update_flags: FramebufferPropertiesUpdateFlags, +) +{ + let Some(resource) = backend_resources.get_resource( + object_store, + framebuffer_object_id, + ObjectKind::Framebuffer, + ) else { + return; + }; + + let BackendResource::Framebuffer { framebuffer } = resource else { + unreachable!(); + }; + + 'if_dt: { + if update_flags.contains(FramebufferPropertiesUpdateFlags::DEPTH_TEXTURE) { + let Some(framebuffer_depth_texture) = &properties.depth_texture else { + framebuffer + .detach_texture(current_context, GlFramebufferAttachment::Depth); + + break 'if_dt; + }; + + let Some(depth_texture_resource) = backend_resources.get_resource( + object_store, + framebuffer_depth_texture.object_id, + ObjectKind::Texture, + ) else { + break 'if_dt; + }; + + let BackendResource::Texture { texture: depth_texture, .. } = + depth_texture_resource + else { + unreachable!(); + }; + + framebuffer.attach_texture( + current_context, + GlFramebufferAttachment::Depth, + depth_texture.clone(), + framebuffer_depth_texture.mipmap_level, + ); + } + } + + if update_flags.contains(FramebufferPropertiesUpdateFlags::COLOR_DRAW_BUFFER) { + framebuffer.set_draw_buffer( + current_context, + properties.color_draw_buffer.map(|color_texture_index| { + GlFramebufferColorAttachment(color_texture_index.0) + }), + ); + } +} + +fn set_shader_binding<'backend_resources>( + backend_resources: &'backend_resources mut BackendResourceStore, + object_store: &ObjectStore, + gl_context: &MaybeCurrentContextWithFns, + shader_object_id: ObjectId, + binding_location: &ShaderBindingLocation, + binding_value: ShaderBindingValue, +) -> Option<&'backend_resources BackendShaderBinding> +{ + if let ShaderBindingValue::Texture(texture_object_id, _) = &binding_value { + let gl_texture = match backend_resources.get_resource( + object_store, + *texture_object_id, + ObjectKind::Texture, + ) { + Some(BackendResource::Texture { texture, .. }) => texture.clone(), + Some(_) => unreachable!(), + None => return None, + }; + + let shader_bindings = match backend_resources.get_resource_mut( + object_store, + shader_object_id, + ObjectKind::ShaderProgram, + ) { + Some(BackendResource::Shader { bindings, .. }) => bindings, + Some(_) => unreachable!(), + None => return None, + }; + + if let Some(prev_binding) = + shader_bindings.remove(ShaderBindingIndex(binding_location.binding_index)) + { + // TODO: Textures should probably also be handled somehow here + if let BackendShaderBinding::Uniform(prev_binding_uniform_buf) = prev_binding + { + prev_binding_uniform_buf.delete(gl_context); + } + } + + return Some( + shader_bindings + .entry(ShaderBindingIndex(binding_location.binding_index)) + .set_or_insert_with(|| BackendShaderBinding::Texture(gl_texture.clone())), + ); + + // if Some(shader_object_id) + // == activated_gl_shader_program + // .as_ref() + // .map(|(obj_id, ..)| *obj_id) + // { + // BackendShaderBinding::Texture(gl_texture.clone()).bind( + // gl_context, + // ShaderBindingIndex(binding_location.binding_index), + // ); + // } + } + + let shader_bindings = match backend_resources.get_resource_mut( + object_store, + shader_object_id, + ObjectKind::ShaderProgram, + ) { + Some(BackendResource::Shader { bindings, .. }) => bindings, + Some(_) => unreachable!(), + None => return None, + }; + + let binding = + match shader_bindings.get(ShaderBindingIndex(binding_location.binding_index)) { + Some(binding @ BackendShaderBinding::Uniform(_)) => binding, + Some(_) | None => { + let uniform_buf = GlBuffer::<u8>::new(gl_context); + + uniform_buf + .init( + gl_context, + binding_location.binding_size, + opengl_bindings::buffer::Usage::Dynamic, + ) + .unwrap(); + + shader_bindings + .entry(ShaderBindingIndex(binding_location.binding_index)) + .set_or_insert_with(|| BackendShaderBinding::Uniform(uniform_buf)) + } + }; + + let BackendShaderBinding::Uniform(binding_uniform_buf) = binding else { + unreachable!(); + }; + + let fvec3_value; + let mut color_value = [0.0; 4]; + + binding_uniform_buf + .store_at_byte_offset( + gl_context, + binding_location.byte_offset, + match binding_value { + ShaderBindingValue::Uint(ref value) => value.as_bytes(), + ShaderBindingValue::Int(ref value) => value.as_bytes(), + ShaderBindingValue::Float(ref value) => value.as_bytes(), + ShaderBindingValue::FVec3(value) => { + fvec3_value = [value.x, value.y, value.z]; + fvec3_value.as_bytes() + } + ShaderBindingValue::Color(value) => match value { + Color::Rgb(value) => { + color_value[..3].copy_from_slice(&[value.r, value.g, value.b]); + + color_value[..3].as_bytes() + } + Color::Rgba(value) => { + color_value[..4] + .copy_from_slice(&[value.r, value.g, value.b, value.a]); + + color_value[..4].as_bytes() + } + }, + ShaderBindingValue::FMat4x4(ref value) => value.items().as_bytes(), + ShaderBindingValue::Texture(_, _) => unreachable!(), + }, + ) + .unwrap(); + + Some(binding) + + // if Some(shader_object_id) + // == activated_gl_shader_program + // .as_ref() + // .map(|(obj_id, ..)| *obj_id) + // { + // BackendShaderBinding::Uniform(binding_uniform_buf.clone()).bind( + // gl_context, + // ShaderBindingIndex(binding_location.binding_index), + // ); + // } +} + +fn draw_mesh( + current_context: &MaybeCurrentContextWithFns, + graphics_mesh: &GraphicsMesh, + opts: &DrawMeshOptions, +) -> Result<(), GlDrawError> +{ + graphics_mesh.vertex_arr.bind(current_context); + + if graphics_mesh.index_buffer.is_some() { + VertexArray::draw_elements( + current_context, + opengl_bindings::vertex_array::DrawElementsOptions { + primitive_kind: PrimitiveKind::Triangles, + element_offset: opts.element_offset, + element_cnt: opts.element_cnt.unwrap_or(graphics_mesh.element_cnt), + vertex_offset: opts.vertex_offset, + }, + )?; + } else { + VertexArray::draw_arrays( + current_context, + PrimitiveKind::Triangles, + opts.vertex_offset, + opts.element_cnt.unwrap_or(graphics_mesh.element_cnt), + )?; + } + + Ok(()) +} + +fn tex_pixel_data_format_into_gl( + tex_pixel_data_format: TexturePixelDataFormat, +) -> GlTexturePixelDataFormat +{ + match tex_pixel_data_format { + TexturePixelDataFormat::Rgb(data_type) => { + GlTexturePixelDataFormat::Rgb(match data_type { + crate::rendering::RgbTextureDataType::UnsignedByte => { + opengl_bindings::texture::RgbDataType::UnsignedByte + } + }) + } + TexturePixelDataFormat::Srgb(data_type) => { + GlTexturePixelDataFormat::Srgb(match data_type { + crate::rendering::SrgbTextureDataType::UnsignedByte => { + opengl_bindings::texture::SrgbDataType::UnsignedByte + } + }) + } + TexturePixelDataFormat::Rgba(data_type) => { + GlTexturePixelDataFormat::Rgba(match data_type { + crate::rendering::RgbaTextureDataType::UnsignedByte => { + opengl_bindings::texture::RgbaDataType::UnsignedByte + } + }) + } + TexturePixelDataFormat::Srgba(data_type) => { + GlTexturePixelDataFormat::Srgba(match data_type { + crate::rendering::SrgbaTextureDataType::UnsignedByte => { + opengl_bindings::texture::SrgbaDataType::UnsignedByte + } + }) + } + TexturePixelDataFormat::Depth(data_type) => { + GlTexturePixelDataFormat::DepthComponent(match data_type { + crate::rendering::DepthTextureDataType::Float32 => { + opengl_bindings::texture::DepthComponentDataType::Float32 + } + }) + } + } +} + +fn create_gl_texture( + curr_gl_context: &MaybeCurrentContextWithFns, + texture_creation: TextureCreation, + texture_pixel_data_format: TexturePixelDataFormat, + texture_properties: &TextureProperties, +) -> Result<GlTexture, GlTextureError> +{ + let gl_texture = match texture_creation { + TextureCreation::Texture2D { size, image } => { + GlTexture::builder().size(size.into()).create_2d( + curr_gl_context, + image.as_deref(), + tex_pixel_data_format_into_gl(texture_pixel_data_format), + ) + } + TextureCreation::CubeMap { size, images } => { + GlTexture::builder().size(size.into()).create_cube_map( + curr_gl_context, + images.as_ref().map(|images| { + images.each_ref().map(|(face, image)| { + (cube_map_texture_face_to_gl(*face), &**image) + }) + }), + tex_pixel_data_format_into_gl(texture_pixel_data_format), + ) + } + }?; + + gl_texture.set_wrap( + curr_gl_context, + texture_wrapping_to_gl(texture_properties.wrap), + ); + + gl_texture.set_magnifying_filter( + curr_gl_context, + texture_filtering_to_gl(texture_properties.magnifying_filter), + ); + + gl_texture.set_minifying_filter( + curr_gl_context, + texture_filtering_to_gl(texture_properties.minifying_filter), + ); + + if let Some(border) = texture_properties.border.as_ref() { + gl_texture.set_float_border_values( + curr_gl_context, + match border { + TextureBorder::RgbaF32(color) => [color.r, color.g, color.b, color.a], + TextureBorder::Depth(depth) => [*depth, 0.0, 0.0, 0.0], + }, + ); + } + + Ok(gl_texture) +} + +fn cube_map_texture_face_to_gl(face: CubeMapTextureFace) -> GlCubeMapTextureFace +{ + match face { + CubeMapTextureFace::PositiveX => GlCubeMapTextureFace::PositiveX, + CubeMapTextureFace::NegativeX => GlCubeMapTextureFace::NegativeX, + CubeMapTextureFace::PositiveY => GlCubeMapTextureFace::PositiveY, + CubeMapTextureFace::NegativeY => GlCubeMapTextureFace::NegativeY, + CubeMapTextureFace::PositiveZ => GlCubeMapTextureFace::PositiveZ, + CubeMapTextureFace::NegativeZ => GlCubeMapTextureFace::NegativeZ, + } +} + +fn create_shader_program( + current_context: &MaybeCurrentContextWithFns, + shader_program: &ShaderProgram, +) -> Result<GlShaderProgram, CreateShaderError> +{ + let shader_program_reflection = shader_program.reflection(0).expect("Not possible"); + + let (vs_entry_point_index, vs_entry_point_reflection) = shader_program_reflection + .entry_points() + .enumerate() + .find(|(_, entry_point)| entry_point.stage() == ShaderStage::Vertex) + .ok_or_else(|| { + CreateShaderError::NoShaderStageEntrypointFound(ShaderStage::Vertex) + })?; + + let vertex_shader_entry_point_code = shader_program + .get_entry_point_code(vs_entry_point_index.try_into().expect( + "Vertex shader entry point index does not fit in 32-bit unsigned int", + )) + .map_err(|err| CreateShaderError::GetShaderEntryPointCodeFailed { + err, + stage: ShaderStage::Vertex, + entrypoint: vs_entry_point_reflection + .name() + .map(|name| name.to_string().into()) + .unwrap_or("(none)".into()), + })?; + + let (fs_entry_point_index, fs_entry_point_reflection) = shader_program_reflection + .entry_points() + .enumerate() + .find(|(_, entry_point)| entry_point.stage() == ShaderStage::Fragment) + .ok_or_else(|| { + CreateShaderError::NoShaderStageEntrypointFound(ShaderStage::Fragment) + })?; + + let fragment_shader_entry_point_code = shader_program + .get_entry_point_code(fs_entry_point_index.try_into().expect( + "Fragment shader entry point index does not fit in 32-bit unsigned int", + )) + .map_err(|err| CreateShaderError::GetShaderEntryPointCodeFailed { + err, + stage: ShaderStage::Fragment, + entrypoint: fs_entry_point_reflection + .name() + .map(|name| name.to_string().into()) + .unwrap_or("(none)".into()), + })?; + + let vertex_shader = GlShader::new(current_context, ShaderKind::Vertex); + + vertex_shader.set_source( + current_context, + &vertex_shader_entry_point_code.as_str().unwrap(), + )?; + + vertex_shader.compile(current_context)?; + + let fragment_shader = GlShader::new(current_context, ShaderKind::Fragment); + + fragment_shader.set_source( + current_context, + &fragment_shader_entry_point_code.as_str().unwrap(), + )?; + + fragment_shader.compile(current_context)?; + + let gl_shader_program = GlShaderProgram::new(current_context); + + gl_shader_program.attach(current_context, &vertex_shader); + gl_shader_program.attach(current_context, &fragment_shader); + + gl_shader_program.link(current_context)?; + + Ok(gl_shader_program) +} + +#[derive(Debug, thiserror::Error)] +enum CreateShaderError +{ + #[error( + "Failed to get code of shader program entry point {entrypoint} of stage {stage:?}" + )] + GetShaderEntryPointCodeFailed + { + #[source] + err: ShaderError, + stage: ShaderStage, + entrypoint: Cow<'static, str>, + }, + + #[error("No entrypoint was found for shader stage {0:?}")] + NoShaderStageEntrypointFound(ShaderStage), + + #[error(transparent)] + ShaderError(#[from] GlShaderError), +} + +// This function not having tracing instrumentation is intentional. It would not provide +// any value +fn opengl_debug_message_cb( + source: MessageSource, + ty: MessageType, + id: u32, + severity: MessageSeverity, + message: &str, +) +{ + macro_rules! emit_log_with_message { + ($level: expr) => { + tracing::event!($level, ?source, ?ty, id, ?severity, "{message}"); + }; + } + + if matches!(severity, MessageSeverity::Notification) { + return; + } + + match ty { + MessageType::Error => { + emit_log_with_message!(tracing::Level::ERROR); + + let backtrace = std::backtrace::Backtrace::capture(); + + if backtrace.status() == std::backtrace::BacktraceStatus::Captured { + tracing::error!("{backtrace}"); + } + } + MessageType::Other => { + emit_log_with_message!(tracing::Level::INFO); + } + _ => { + emit_log_with_message!(tracing::Level::WARN); + } + }; +} + +#[inline] +fn texture_wrapping_to_gl(texture_wrapping: TextureWrapping) -> GlTextureWrapping +{ + match texture_wrapping { + TextureWrapping::Repeat => GlTextureWrapping::Repeat, + TextureWrapping::MirroredRepeat => GlTextureWrapping::MirroredRepeat, + TextureWrapping::ClampToEdge => GlTextureWrapping::ClampToEdge, + TextureWrapping::ClampToBorder => GlTextureWrapping::ClampToBorder, + } +} + +#[inline] +fn texture_filtering_to_gl(texture_filtering: TextureFiltering) -> GlTextureFiltering +{ + match texture_filtering { + TextureFiltering::Linear => GlTextureFiltering::Linear, + TextureFiltering::Nearest => GlTextureFiltering::Nearest, + } +} + +impl<Value: ReprC + Copy> From<Vec2<Value>> for opengl_bindings::data_types::Vec2<Value> +{ + fn from(vec2: Vec2<Value>) -> Self + { + Self { x: vec2.x, y: vec2.y } + } +} + +impl<Value: ReprC + IntoBytes + Copy> From<Vec3<Value>> + for opengl_bindings::data_types::Vec3<Value> +{ + fn from(vec3: Vec3<Value>) -> Self + { + Self { x: vec3.x, y: vec3.y, z: vec3.z } + } +} + +impl<Value: Copy> From<Dimens<Value>> for opengl_bindings::data_types::Dimens<Value> +{ + fn from(dimens: Dimens<Value>) -> Self + { + Self { + width: dimens.width, + height: dimens.height, + } + } +} + +fn polygon_mode_to_gl(mode: PolygonMode) -> GlPolygonMode +{ + match mode { + PolygonMode::Point => GlPolygonMode::Point, + PolygonMode::Fill => GlPolygonMode::Fill, + PolygonMode::Line => GlPolygonMode::Line, + } +} + +fn polygon_mode_face_to_gl(face: PolygonModeFace) -> GlPolygonModeFace +{ + match face { + PolygonModeFace::Front => GlPolygonModeFace::Front, + PolygonModeFace::Back => GlPolygonModeFace::Back, + PolygonModeFace::FrontAndBack => GlPolygonModeFace::FrontAndBack, + } +} + +fn blending_factor_to_gl(blending_factor: BlendingFactor) -> GlBlendingFactor +{ + match blending_factor { + BlendingFactor::Zero => GlBlendingFactor::Zero, + BlendingFactor::One => GlBlendingFactor::One, + BlendingFactor::SrcColor => GlBlendingFactor::SrcColor, + BlendingFactor::OneMinusSrcColor => GlBlendingFactor::OneMinusSrcColor, + BlendingFactor::DstColor => GlBlendingFactor::DstColor, + BlendingFactor::OneMinusDstColor => GlBlendingFactor::OneMinusDstColor, + BlendingFactor::SrcAlpha => GlBlendingFactor::SrcAlpha, + BlendingFactor::OneMinusSrcAlpha => GlBlendingFactor::OneMinusSrcAlpha, + BlendingFactor::DstAlpha => GlBlendingFactor::DstAlpha, + BlendingFactor::OneMinusDstAlpha => GlBlendingFactor::OneMinusDstAlpha, + BlendingFactor::ConstantColor => GlBlendingFactor::ConstantColor, + BlendingFactor::OneMinusConstantColor => GlBlendingFactor::OneMinusConstantColor, + BlendingFactor::ConstantAlpha => GlBlendingFactor::ConstantAlpha, + BlendingFactor::OneMinusConstantAlpha => GlBlendingFactor::OneMinusConstantAlpha, + } +} + +fn blending_equation_to_gl(blending_equation: BlendingEquation) -> GlBlendingEquation +{ + match blending_equation { + BlendingEquation::Add => GlBlendingEquation::Add, + BlendingEquation::Subtract => GlBlendingEquation::Subtract, + BlendingEquation::ReverseSubtract => GlBlendingEquation::ReverseSubtract, + BlendingEquation::Min => GlBlendingEquation::Min, + BlendingEquation::Max => GlBlendingEquation::Max, + } +} + +trait IntMapEntryExt<'map, Value> +{ + fn set_or_insert_with(self, func: impl FnOnce() -> Value) -> &'map mut Value; +} + +impl<'map, Key, Value> IntMapEntryExt<'map, Value> for intmap::Entry<'map, Key, Value> +where + Key: intmap::IntKey, + Value: 'map, +{ + fn set_or_insert_with(self, func: impl FnOnce() -> Value) -> &'map mut Value + { + match self { + Self::Occupied(occupied) => { + let value = occupied.into_mut(); + + *value = func(); + + value + } + Self::Vacant(vacant) => vacant.insert(func()), + } + } +} diff --git a/engine/src/rendering/backend/opengl/glutin_compat.rs b/engine/src/rendering/backend/opengl/glutin_compat.rs new file mode 100644 index 0000000..27f82ad --- /dev/null +++ b/engine/src/rendering/backend/opengl/glutin_compat.rs @@ -0,0 +1,264 @@ +// Original file: +// https://github.com/rust-windowing/glutin/blob/ +// 0433af9018febe0696c485ed9d66c40dad41f2d4/glutin-winit/src/lib.rs +// +// Copyright © 2022 Kirill Chibisov +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the “Software”), to deal +// in the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +// of the Software, and to permit persons to whom the Software is furnished to do +// so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +//! This library provides helpers for cross-platform [`glutin`] bootstrapping +//! with [`winit`]. + +#![deny(rust_2018_idioms)] +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(clippy::all)] +#![deny(missing_debug_implementations)] +#![deny(missing_docs)] +#![cfg_attr(clippy, deny(warnings))] + +use glutin::config::{Config, ConfigTemplateBuilder}; +use glutin::display::{Display, DisplayApiPreference}; +use glutin::error::Error as GlutinError; +use glutin::prelude::*; +use raw_window_handle::{DisplayHandle, RawWindowHandle, WindowHandle}; + +use crate::windowing::window::CreationAttributes as WindowCreationAttributes; + +#[cfg(all(not(windows), not(target_os = "macos"), not(target_os = "linux")))] +compile_error!("Unsupported platform"); + +#[cfg(target_family = "wasm")] +compile_error!("Wasm targets are not supported"); + +/// The helper to perform [`Display`] creation and OpenGL platform +/// bootstrapping with the help of [`winit`] with little to no platform specific +/// code. +/// +/// This is only required for the initial setup. If you want to create +/// additional windows just use the [`finalize_window`] function and the +/// configuration you've used either for the original window or picked with the +/// existing [`Display`]. +/// +/// [`winit`]: winit +/// [`Display`]: glutin::display::Display +#[derive(Default, Debug, Clone)] +pub struct DisplayBuilder +{ + preference: ApiPreference, + window_attributes: WindowCreationAttributes, +} + +impl DisplayBuilder +{ + /// Create new display builder. + pub fn new() -> Self + { + Default::default() + } + + /// The preference in picking the configuration. + #[allow(dead_code)] + pub fn with_preference(mut self, preference: ApiPreference) -> Self + { + self.preference = preference; + self + } + + /// The window attributes to use when building a window. + /// + /// By default no window is created. + pub fn with_window_attributes( + mut self, + window_creation_attrs: WindowCreationAttributes, + ) -> Self + { + self.window_attributes = window_creation_attrs; + self + } + + /// Initialize the OpenGL platform and create a compatible window to use + /// with it when the [`WindowAttributes`] was passed with + /// [`Self::with_window_attributes()`]. It's optional, since on some + /// platforms like `Android` it is not available early on, so you want to + /// find configuration and later use it with the [`finalize_window`]. + /// But if you don't care about such platform you can always pass + /// [`WindowAttributes`]. + /// + /// # Api-specific + /// + /// **WGL:** - [`WindowAttributes`] **must** be passed in + /// [`Self::with_window_attributes()`] if modern OpenGL(ES) is desired, + /// otherwise only builtin functions like `glClear` will be available. + pub fn build<ConfigPickerFn>( + self, + window_handle: Option<WindowHandle<'_>>, + display_handle: &DisplayHandle<'_>, + template_builder: ConfigTemplateBuilder, + config_picker_fn: ConfigPickerFn, + ) -> Result<(WindowCreationAttributes, Config), Error> + where + ConfigPickerFn: FnOnce(Box<dyn Iterator<Item = Config> + '_>) -> Option<Config>, + { + // XXX with WGL backend window should be created first. + let raw_window_handle = if cfg!(windows) { + let Some(window_handle) = window_handle else { + return Err(Error::WindowRequired); + }; + + Some(window_handle.as_raw()) + } else { + None + }; + + let gl_display = + create_display(display_handle, self.preference, raw_window_handle) + .map_err(Error::CreateDisplayFailed)?; + + // XXX the native window must be passed to config picker when WGL is used + // otherwise very limited OpenGL features will be supported. + let template_builder = if let Some(raw_window_handle) = + raw_window_handle.filter(|_| cfg!(windows)) + { + template_builder.compatible_with_native_window(raw_window_handle) + } else { + template_builder + }; + + let template = template_builder.build(); + + // SAFETY: The RawWindowHandle passed on the config template + // (when cfg(windows)) will always point to a valid object since it is + // derived from the window_handle argument which when Some is a WindowHandle and + // WindowHandles always point to a valid object + let gl_configs = unsafe { gl_display.find_configs(template) } + .map_err(Error::FindConfigsFailed)?; + + let picked_gl_config = + config_picker_fn(gl_configs).ok_or(Error::NoConfigPicked)?; + + let window_attrs = cfg_select! { + windows => { self.window_attributes } + _ => { + finalize_window_creation_attrs(self.window_attributes, &picked_gl_config) + } + }; + + Ok((window_attrs, picked_gl_config)) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("Failed to create display")] + CreateDisplayFailed(#[source] GlutinError), + + #[error("Failed to find configs")] + FindConfigsFailed(#[source] GlutinError), + + #[error("No config was picked by config picker function")] + NoConfigPicked, + + #[error("Window required for building display on current platform")] + WindowRequired, +} + +fn create_display( + display_handle: &DisplayHandle<'_>, + _api_preference: ApiPreference, + _raw_window_handle: Option<RawWindowHandle>, +) -> Result<Display, GlutinError> +{ + let preference = cfg_select! { + windows => { + match _api_preference { + ApiPreference::PreferEgl => { + DisplayApiPreference::EglThenWgl(_raw_window_handle) + } + ApiPreference::FallbackEgl => { + DisplayApiPreference::WglThenEgl(_raw_window_handle) + } + } + } + target_os = "linux" => { + match _api_preference { + ApiPreference::PreferEgl => DisplayApiPreference::EglThenGlx(Box::new( + crate::windowing::window::platform::x11::register_xlib_error_hook, + )), + ApiPreference::FallbackEgl => DisplayApiPreference::GlxThenEgl(Box::new( + crate::windowing::window::platform::x11::register_xlib_error_hook, + )), + } + } + target_os = "macos" => { DisplayApiPreference::Cgl } + }; + + let handle = display_handle.as_raw(); + + unsafe { Ok(Display::new(handle, preference)?) } +} + +/// Finalize [`Window`] creation by applying the options from the [`Config`], be +/// aware that it could remove incompatible options from the window builder like +/// `transparency`, when the provided config doesn't support it. +/// +/// [`Window`]: winit::window::Window +/// [`Config`]: glutin::config::Config +#[cfg(not(windows))] +fn finalize_window_creation_attrs( + mut attributes: WindowCreationAttributes, + gl_config: &Config, +) -> WindowCreationAttributes +{ + // Disable transparency if the end config doesn't support it. + if gl_config.supports_transparency() == Some(false) { + attributes = attributes.with_transparent(false); + } + + #[cfg(target_os = "linux")] + if let Some(x11_visual) = glutin::platform::x11::X11GlConfigExt::x11_visual(gl_config) + { + return attributes.with_x_visual_id(Some( + x11_visual.visual_id() as crate::windowing::window::XVisualID + )); + } + + attributes +} + +/// Simplified version of the [`DisplayApiPreference`] which is used to simplify +/// cross platform window creation. +/// +/// To learn about platform differences the [`DisplayApiPreference`] variants. +/// +/// [`DisplayApiPreference`]: glutin::display::DisplayApiPreference +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum ApiPreference +{ + /// Prefer `EGL` over system provider like `GLX` and `WGL`. + PreferEgl, + + /// Fallback to `EGL` when failed to create the system profile. + /// + /// This behavior is used by default. However consider using + /// [`Self::PreferEgl`] if you don't care about missing EGL features. + #[default] + FallbackEgl, +} diff --git a/engine/src/rendering/backend/opengl/graphics_mesh.rs b/engine/src/rendering/backend/opengl/graphics_mesh.rs new file mode 100644 index 0000000..a3f4197 --- /dev/null +++ b/engine/src/rendering/backend/opengl/graphics_mesh.rs @@ -0,0 +1,259 @@ +use std::hint::cold_path; + +use opengl_bindings::buffer::{Buffer as GlBuffer, Usage as GlBufferUsage}; +use opengl_bindings::vertex_array::{ + AttributeFormat as GlVertexArrayAttributeFormat, + BindVertexBufferError as GlVertexArrayBindVertexBufferError, + DataType as GlVertexArrayDataType, + VertexArray as GlVertexArray, + VertexBufferSpec as GlVertexArrayVertexBufferSpec, +}; +use opengl_bindings::MaybeCurrentContextWithFns as GlCurrentContextWithFns; + +use crate::mesh::vertex_buffer::VertexAttrProperties as MeshVertexAttrProperties; +use crate::mesh::{Mesh, VertexAttrType}; +use crate::rendering::shader::{ + VertexDescription as ShaderVertexDescription, + VertexInputSemName, +}; +use crate::rendering::MeshUsage; + +const VERTEX_BUF_BINDING_INDEX: u32 = 0; + +#[derive(Debug)] +pub struct GraphicsMesh +{ + /// Vertex and index buffer has to live as long as the vertex array + vertex_buffer: GlBuffer<u8>, + vertex_attr_props: Vec<MeshVertexAttrProperties>, + last_max_vertex_attr_index: u32, + pub index_buffer: Option<GlBuffer<u32>>, + pub element_cnt: u32, + pub vertex_arr: GlVertexArray, +} + +impl GraphicsMesh +{ + #[tracing::instrument(skip_all)] + pub fn new( + current_context: &GlCurrentContextWithFns, + mesh: &Mesh, + mesh_usage: MeshUsage, + ) -> Result<Self, Error> + { + let buffer_usage = mesh_usage_to_gl_buffer_usage(mesh_usage); + + let vertex_arr = GlVertexArray::new(current_context); + let vertex_buffer = GlBuffer::new(current_context); + + vertex_buffer + .store(current_context, mesh.vertex_buf().as_bytes(), buffer_usage) + .map_err(Error::StoreVerticesFailed)?; + + if let Err(err) = vertex_arr.bind_vertex_buffer( + current_context, + VERTEX_BUF_BINDING_INDEX, + &vertex_buffer, + GlVertexArrayVertexBufferSpec { + offset: 0, + vertex_size: mesh.vertex_buf().vertex_size(), + }, + ) { + match err { + GlVertexArrayBindVertexBufferError::OffsetValueTooLarge { + value: _, + max_value: _, + } => unreachable!(), + GlVertexArrayBindVertexBufferError::VertexSizeValueTooLarge { + value, + max_value, + } => { + panic!( + "Size of vertex ({}) is too large. Must be less than {max_value}", + value + ); + } + } + } + + if let Some(indices) = mesh.indices() { + let index_buffer = GlBuffer::new(current_context); + + index_buffer + .store(current_context, indices, buffer_usage) + .map_err(Error::StoreIndicesFailed)?; + + vertex_arr.bind_element_buffer(current_context, &index_buffer); + + return Ok(Self { + vertex_buffer: vertex_buffer, + vertex_attr_props: mesh.vertex_buf().vertex_attr_props().to_vec(), + last_max_vertex_attr_index: 0, + index_buffer: Some(index_buffer), + element_cnt: indices + .len() + .try_into() + .expect("Mesh index count does not fit into a 32-bit unsigned int"), + vertex_arr, + }); + } + + Ok(Self { + vertex_buffer: vertex_buffer, + vertex_attr_props: mesh.vertex_buf().vertex_attr_props().to_vec(), + last_max_vertex_attr_index: 0, + index_buffer: None, + element_cnt: mesh + .vertex_buf() + .len() + .try_into() + .expect("Mesh vertex count does not fit into a 32-bit unsigned int"), + vertex_arr, + }) + } + + pub fn update( + &mut self, + current_context: &GlCurrentContextWithFns, + mesh: &Mesh, + mesh_usage: MeshUsage, + ) -> Result<(), Error> + { + let buffer_usage = mesh_usage_to_gl_buffer_usage(mesh_usage); + + self.vertex_buffer + .store(current_context, mesh.vertex_buf().as_bytes(), buffer_usage) + .map_err(Error::StoreVerticesFailed)?; + + if let Some(indices) = mesh.indices() { + let index_buffer = self + .index_buffer + .get_or_insert_with(|| GlBuffer::new(current_context)); + + index_buffer + .store(current_context, indices, buffer_usage) + .map_err(Error::StoreIndicesFailed)?; + + self.vertex_arr + .bind_element_buffer(current_context, &index_buffer); + + self.element_cnt = indices + .len() + .try_into() + .expect("Mesh index count does not fit into a 32-bit unsigned int"); + + return Ok(()); + } + + self.element_cnt = mesh + .vertex_buf() + .len() + .try_into() + .expect("Mesh vertex count does not fit into a 32-bit unsigned int"); + + Ok(()) + } + + pub fn update_vertex_attrs_for_shader( + &mut self, + curr_gl_ctx: &GlCurrentContextWithFns, + shader_vertex_desc: &ShaderVertexDescription, + ) -> Result<(), VertexAttrsUpdatingError> + { + for index in 0..self.last_max_vertex_attr_index { + self.vertex_arr.disable_attrib(curr_gl_ctx, index); + } + + let mut max_vertex_attr_index = 0u32; + + for vertex_input_desc in &*shader_vertex_desc.inputs { + let Some(vertex_attr_props) = + self.vertex_attr_props.iter().find(|vertex_attr_props| { + vertex_input_desc + .semantic_name + .matches_vertex_label(&vertex_attr_props.label) + }) + else { + cold_path(); + return Err(VertexAttrsUpdatingError::MissingVertexAttr( + vertex_input_desc.semantic_name.clone(), + )); + }; + + let attrib_index: u32 = vertex_input_desc.index.try_into().unwrap(); + + self.vertex_arr.enable_attrib(curr_gl_ctx, attrib_index); + + self.vertex_arr.set_attrib_format( + curr_gl_ctx, + attrib_index, + match &vertex_attr_props.ty { + VertexAttrType::Float32 => GlVertexArrayAttributeFormat { + data_type: GlVertexArrayDataType::Float, + count: 1, + normalized: false, + offset: vertex_attr_props.byte_offset.try_into().unwrap(), + }, + VertexAttrType::Float32Array { length } => { + GlVertexArrayAttributeFormat { + data_type: GlVertexArrayDataType::Float, + count: (*length).try_into().unwrap(), + normalized: false, + offset: vertex_attr_props.byte_offset.try_into().unwrap(), + } + } + }, + ); + + self.vertex_arr.set_attrib_vertex_buf_binding( + curr_gl_ctx, + attrib_index, + VERTEX_BUF_BINDING_INDEX, + ); + + max_vertex_attr_index = max_vertex_attr_index.max(attrib_index); + } + + self.last_max_vertex_attr_index = max_vertex_attr_index; + + Ok(()) + } + + pub fn destroy(&self, curr_gl_ctx: &GlCurrentContextWithFns) + { + self.vertex_arr.delete(curr_gl_ctx); + self.vertex_buffer.delete(curr_gl_ctx); + + if let Some(index_buffer) = &self.index_buffer { + index_buffer.delete(curr_gl_ctx); + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error +{ + #[error("Failed to store vertices in vertex buffer")] + StoreVerticesFailed(#[source] opengl_bindings::buffer::Error), + + #[error("Failed to store indices in index buffer")] + StoreIndicesFailed(#[source] opengl_bindings::buffer::Error), +} + +#[derive(Debug, thiserror::Error)] +pub enum VertexAttrsUpdatingError +{ + #[error( + "Mesh is missing equivalent vertex attribute for shader's vertex input: {0}" + )] + MissingVertexAttr(VertexInputSemName), +} + +fn mesh_usage_to_gl_buffer_usage(mesh_usage: MeshUsage) -> GlBufferUsage +{ + match mesh_usage { + MeshUsage::Stream => GlBufferUsage::Stream, + MeshUsage::Static => GlBufferUsage::Static, + MeshUsage::Dynamic => GlBufferUsage::Dynamic, + } +} diff --git a/engine/src/rendering/blending.rs b/engine/src/rendering/blending.rs new file mode 100644 index 0000000..9ae2f82 --- /dev/null +++ b/engine/src/rendering/blending.rs @@ -0,0 +1,89 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Config +{ + pub source_factor: Factor, + pub destination_factor: Factor, + pub equation: Equation, +} + +impl Default for Config +{ + fn default() -> Self + { + Self { + source_factor: Factor::One, + destination_factor: Factor::Zero, + equation: Equation::default(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum Factor +{ + /// Factor will be the RGBA color `(0,0,0,0)` + Zero, + + /// Factor will be the RGBA color `(1,1,1,1)` + One, + + /// Factor will be the source color + SrcColor, + + /// Factor will be the RGBA color `(1,1,1,1) - source color` + OneMinusSrcColor, + + /// Factor will be the destination color + DstColor, + + /// Factor will be the RGBA color `(1,1,1,1) - destination color` + OneMinusDstColor, + + /// Factor will be the alpha component of the source color. + SrcAlpha, + + /// Factor will be the RGBA color `(1,1,1,1) - source color alpha` + OneMinusSrcAlpha, + + /// Factor will be the alpha component of the destination color. + DstAlpha, + + /// Factor will be the RGBA color `(1,1,1,1) - destination color alpha` + OneMinusDstAlpha, + + /// Factor will be the constant color + ConstantColor, + + /// Factor will be the RGBA color `(1,1,1,1) - constant color` + OneMinusConstantColor, + + /// Factor will be the alpha component of the constant color. + ConstantAlpha, + + /// Factor will be the RGBA color `(1,1,1,1) - constant color alpha` + OneMinusConstantAlpha, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum Equation +{ + /// The destination color and source color is added to each other in the blend + /// function + #[default] + Add, + + /// The destination color is subtracted from the source color in the blend function + Subtract, + + /// The source color is subtracted from the destination color in the blend function + ReverseSubtract, + + /// The blend function will take the component-wise minimum of the destination color + /// and the source color + Min, + + /// The blend function will take the component-wise maximum of the destination color + /// and the source color + Max, +} diff --git a/engine/src/rendering/main_render_pass.rs b/engine/src/rendering/main_render_pass.rs new file mode 100644 index 0000000..c762231 --- /dev/null +++ b/engine/src/rendering/main_render_pass.rs @@ -0,0 +1,1006 @@ +use std::path::Path; +use std::sync::LazyLock; + +use ecs::actions::Actions; +use ecs::component::local::Local; +use ecs::error::Context as _; +use ecs::pair::ChildOf; +use ecs::query::term::{Traverse, TraverseUp}; +use ecs::uid::Uid; +use ecs::Component; + +use crate::asset::{Assets, Label as AssetLabel}; +use crate::camera::{Active as ActiveCamera, Camera}; +use crate::color::Rgba; +use crate::data_types::dimens::{Dimens, Dimens3}; +use crate::draw_flags::{DrawFlags, NoDraw, PolygonModeConfig}; +use crate::ecs::query::term::{With, Without}; +use crate::ecs::sole::Single; +use crate::ecs::Query; +use crate::error; +use crate::image::Image; +use crate::lighting::{ + DirectionalLight, + Environmental as EnvironmentalLighting, + PointLight, +}; +use crate::material::{Flags as MaterialFlags, Material}; +use crate::mesh::Mesh; +use crate::model::{MaterialSearchResult, Model}; +use crate::projection::{ClipVolume as ProjectionClipVolume, Perspective, Projection}; +use crate::rendering::object::{Id as ObjectId, Store as ObjectStore}; +use crate::rendering::shader::cursor::{ + BindingTextureKind as ShaderBindingTextureKind, + BindingValue as ShaderBindingValue, + Cursor as ShaderCursor, +}; +use crate::rendering::shader::{ + Context as ShaderContext, + EntrypointFlags as ShaderEntrypointFlags, + ModuleSource as ShaderModuleSource, + Program as ShaderProgram, +}; +use crate::rendering::{ + AssetOrValue, + BufferClearMask, + Command, + DepthFunction, + DrawMeshOptions, + DrawProperties, + DrawPropertiesUpdateFlags, + MeshUsage, + RenderPass, + RenderPasses, + RgbaTextureDataType, + Surface, + TargetWindow, + TextureCreation, + TexturePixelDataFormat, +}; +use crate::scene::{Active as ActiveScene, Scene}; +use crate::sky_box::SkyBox; +use crate::texture::{ + Filtering as TextureFiltering, + Properties as TextureProperties, + Texture, + Wrapping as TextureWrapping, +}; +use crate::transform::Transform; +use crate::vector::Vec3; +use crate::windowing::window::Window; + +type RenderableEntity<'a> = ( + &'a Model, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + Option<&'a MaterialFlags>, + Option<&'a Transform>, + Option<&'a DrawFlags>, + Without<NoDraw>, +); + +pub static SKY_BOX_SHADER_ASSET_LABEL: LazyLock<AssetLabel> = + LazyLock::new(|| AssetLabel { + path: Path::new("").into(), + name: Some("sky_box_shader".into()), + }); + +pub static MAIN_3D_SHADER_ASSET_LABEL: LazyLock<AssetLabel> = + LazyLock::new(|| AssetLabel { + path: Path::new("").into(), + name: Some("main_3d_shader".into()), + }); + +struct SkyBoxIds +{ + texture_object: ObjectId, + mesh_object: ObjectId, +} + +#[derive(Debug, Default, Component)] +pub struct MiscObjectIds +{ + white_1x1_tex_obj_id: Option<ObjectId>, +} + +#[tracing::instrument(skip_all)] +pub fn add_main_render_pass( + renderable_query: Query<RenderableEntity<'_>>, + camera_query: Query<( + &Camera, + &Transform, + &ActiveCamera, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + )>, + window_query: Query<(&Window, &Surface, With<TargetWindow>)>, + scene_query: Query<( + Option<&EnvironmentalLighting>, + Option<&SkyBox>, + Option<&SkyBoxState>, + With<Scene>, + With<ActiveScene>, + )>, + point_light_query: Query<( + &PointLight, + &Transform, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + )>, + directional_light_query: Query<( + &DirectionalLight, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + )>, + shader_context: Single<ShaderContext>, + mut assets: Single<Assets>, + mut render_passes: Single<RenderPasses>, + mut object_store: Single<ObjectStore>, + mut actions: Actions, + mut misc_object_ids: Local<MiscObjectIds>, +) -> Result<(), crate::Error> +{ + let assets = assets.get_mut()?; + let shader_context = shader_context.get()?; + let render_passes = render_passes.get_mut()?; + let object_store = object_store.get_mut()?; + + let Some((scene_ent_id, (scene_env_lighting, scene_skybox, scene_skybox_state))) = + scene_query.iter_with_euids().next() + else { + return Ok(()); + }; + + let Some((camera, camera_transform, _)) = camera_query.iter().next() else { + tracing::trace!("No active camera"); + return Ok(()); + }; + + let render_pass = render_passes.passes.push_front_mut(RenderPass { + commands: Vec::with_capacity(30), + draw_properties: DrawProperties::default(), + }); + + for (model, ..) in &renderable_query { + add_renderable_creation_commands(assets, object_store, render_pass, &model); + } + + let shaders = + match get_or_create_shaders(assets, shader_context, object_store, render_pass) { + Ok(ShadersCreationStatus::Ready(ready_shaders)) => ready_shaders, + Ok(ShadersCreationStatus::ProcessingRequired) => return Ok(()), + Err(err) => { + tracing::error!("Failed to create shaders: {err}"); + return Ok(()); + } + }; + + let white_1x1_tex_obj_id = + *misc_object_ids.white_1x1_tex_obj_id.get_or_insert_with(|| { + create_white_1x1_texture_object(object_store, render_pass) + }); + + let main_3d_shader_cursor = ShaderCursor::new( + shaders + .main_3d_shader_program + .reflection(0) + .context("Unable to get reflection for main 3D shader")? + .global_params_var_layout() + .ok_or_else(|| { + crate::Error::message( + "Unable to get reflection for main 3D shader's global parameters", + ) + })?, + ); + + add_set_3d_shader_point_light_bindings( + render_pass, + &point_light_query, + shaders.main_3d_shader_obj_id, + &main_3d_shader_cursor, + )?; + + add_set_3d_shader_dir_lights_bindings( + render_pass, + &directional_light_query, + shaders.main_3d_shader_obj_id, + &main_3d_shader_cursor, + )?; + + for (window, window_surface) in &window_query { + render_pass + .commands + .push(Command::MakeCurrent(window_surface.id)); + + add_set_3d_shader_camera_bindings( + render_pass, + (&camera, &camera_transform), + &window, + shaders.main_3d_shader_obj_id, + &main_3d_shader_cursor, + )?; + + let mut buf_clear_mask = BufferClearMask::DEPTH; + + buf_clear_mask.set(BufferClearMask::COLOR, scene_skybox.is_none()); + + render_pass + .commands + .push(Command::ClearBuffers(buf_clear_mask)); + + for (model, material_flags, transform, draw_flags) in &renderable_query { + let Some(model_spec) = assets.get(&model.spec_asset) else { + continue; + }; + + let Some(mesh_asset) = &model_spec.mesh_asset else { + continue; + }; + + if assets.get(mesh_asset).is_none() { + continue; + } + + let model_material = match model_spec.find_first_material(&assets) { + MaterialSearchResult::Found(model_material_asset) + if let Some(model_material) = assets.get(&model_material_asset) => + { + model_material + } + MaterialSearchResult::Found(_) | MaterialSearchResult::NotFound => { + continue; + } + MaterialSearchResult::NoMaterials => &Material::builder().build(), + }; + + if model_material + .textures() + .any(|texture_asset| !assets.is_loaded_and_has_type(&texture_asset)) + { + continue; + } + + add_set_3d_shader_renderable_bindings( + render_pass, + ( + model_material, + material_flags.as_deref(), + transform.as_deref(), + ), + scene_env_lighting.as_deref(), + shaders.main_3d_shader_obj_id, + &main_3d_shader_cursor, + white_1x1_tex_obj_id, + )?; + + render_pass + .commands + .push(Command::ActivateShader(shaders.main_3d_shader_obj_id)); + + if let Some(draw_flags) = draw_flags.as_deref().and_then(|draw_flags| { + if draw_flags.polygon_mode_config != PolygonModeConfig::default() { + Some(draw_flags) + } else { + None + } + }) { + render_pass.commands.push(Command::UpdateDrawProperties( + DrawProperties { + polygon_mode_config: draw_flags.polygon_mode_config.clone(), + ..Default::default() + }, + DrawPropertiesUpdateFlags::POLYGON_MODE_CONFIG, + )); + } + + render_pass.commands.push(Command::DrawMesh( + ObjectId::Asset(mesh_asset.id()), + DrawMeshOptions::default(), + )); + + if draw_flags.as_deref().is_some_and(|draw_flags| { + draw_flags.polygon_mode_config != PolygonModeConfig::default() + }) { + render_pass.commands.push(Command::UpdateDrawProperties( + DrawProperties { + polygon_mode_config: PolygonModeConfig::default(), + ..Default::default() + }, + DrawPropertiesUpdateFlags::POLYGON_MODE_CONFIG, + )); + } + } + + if let Some(scene_skybox) = &scene_skybox { + let Some(sky_box_ids) = load_sky_box( + scene_ent_id, + &scene_skybox, + scene_skybox_state.as_deref(), + assets, + object_store, + render_pass, + &mut actions, + )? + else { + continue; + }; + + add_sky_box_commands( + render_pass, + &sky_box_ids, + &camera, + &camera_transform, + &window, + shaders.sky_box_shader_obj_id, + shaders.sky_box_shader_program, + )?; + } + } + + Ok(()) +} + +fn create_white_1x1_texture_object( + object_store: &mut ObjectStore, + render_pass: &mut RenderPass, +) -> ObjectId +{ + let white_1x1_tex_obj_id = ObjectId::new_sequential(); + + object_store.insert_pending(white_1x1_tex_obj_id); + + render_pass.commands.push(Command::CreateTexture { + obj_id: white_1x1_tex_obj_id, + pixel_data_format: TexturePixelDataFormat::Rgba( + RgbaTextureDataType::UnsignedByte, + ), + creation: TextureCreation::Texture2D { + size: Dimens { width: 1, height: 1 }, + image: Some( + Image::from_color(Rgba::<u8>::white(), Dimens { width: 1, height: 1 }) + .into_bytes() + .into_boxed_slice(), + ), + }, + properties: TextureProperties::default(), + }); + + white_1x1_tex_obj_id +} + +#[derive(Debug)] +enum ShadersCreationStatus<'shader_ctx> +{ + ProcessingRequired, + Ready(ReadyShaders<'shader_ctx>), +} + +#[derive(Debug)] +struct ReadyShaders<'shader_ctx> +{ + main_3d_shader_obj_id: ObjectId, + main_3d_shader_program: &'shader_ctx ShaderProgram, + sky_box_shader_obj_id: ObjectId, + sky_box_shader_program: &'shader_ctx ShaderProgram, +} + +fn get_or_create_shaders<'shader_ctx>( + assets: &mut Assets, + shader_context: &'shader_ctx ShaderContext, + object_store: &mut ObjectStore, + render_pass: &mut RenderPass, +) -> Result<ShadersCreationStatus<'shader_ctx>, crate::Error> +{ + let main_3d_shader_asset = assets + .get_handle_to_loaded::<ShaderModuleSource>(MAIN_3D_SHADER_ASSET_LABEL.clone()) + .or_else(|| { + assets.store_with_label( + MAIN_3D_SHADER_ASSET_LABEL.clone(), + ShaderModuleSource { + name: "main_3d_shader.slang".into(), + file_path: Path::new("@engine/main_3d_shader").into(), + source: include_str!("../../res/main_3d_shader.slang").into(), + link_entrypoints: ShaderEntrypointFlags::VERTEX + | ShaderEntrypointFlags::FRAGMENT, + }, + ); + + None + }); + + let sky_box_shader_asset = assets + .get_handle_to_loaded::<ShaderModuleSource>(SKY_BOX_SHADER_ASSET_LABEL.clone()) + .or_else(|| { + assets.store_with_label( + SKY_BOX_SHADER_ASSET_LABEL.clone(), + ShaderModuleSource { + name: "sky_box_shader.slang".into(), + file_path: Path::new("@engine/sky_box_shader").into(), + source: include_str!("../../res/sky_box_shader.slang").into(), + link_entrypoints: ShaderEntrypointFlags::VERTEX + | ShaderEntrypointFlags::FRAGMENT, + }, + ); + None + }); + + let (Some(main_3d_shader_asset), Some(sky_box_shader_asset)) = + (main_3d_shader_asset, sky_box_shader_asset) + else { + return Ok(ShadersCreationStatus::ProcessingRequired); + }; + + let main_3d_shader_program = shader_context.get_program(&main_3d_shader_asset.id()); + + let main_3d_shader_program = main_3d_shader_program.ok_or_else(|| { + error!("Shader context doesn't have a program for main 3D shader") + })?; + + let main_3d_shader_obj_id = ObjectId::Asset(main_3d_shader_asset.id()); + + if !object_store.contains_maybe_pending_with_id(&main_3d_shader_obj_id) { + object_store.insert_pending(main_3d_shader_obj_id); + + render_pass.commands.push(Command::CreateShaderProgram( + main_3d_shader_obj_id, + main_3d_shader_program.clone(), + )); + } + + let sky_box_shader_program = shader_context.get_program(&sky_box_shader_asset.id()); + + let sky_box_shader_program = sky_box_shader_program.ok_or_else(|| { + error!("Shader context doesn't have a program for sky box shader") + })?; + + let sky_box_shader_obj_id = ObjectId::Asset(sky_box_shader_asset.id()); + + if !object_store.contains_maybe_pending_with_id(&sky_box_shader_obj_id) { + object_store.insert_pending(sky_box_shader_obj_id); + + render_pass.commands.push(Command::CreateShaderProgram( + sky_box_shader_obj_id, + sky_box_shader_program.clone(), + )); + } + + Ok(ShadersCreationStatus::Ready(ReadyShaders { + main_3d_shader_obj_id, + main_3d_shader_program, + sky_box_shader_obj_id, + sky_box_shader_program, + })) +} + +fn add_renderable_creation_commands( + assets: &Assets, + object_store: &mut ObjectStore, + render_pass: &mut RenderPass, + model: &Model, +) +{ + let Some(model_spec) = assets.get(&model.spec_asset) else { + return; + }; + + let Some(mesh_asset) = &model_spec.mesh_asset else { + return; + }; + + if assets.get(mesh_asset).is_none() { + return; + } + + debug_assert!(model_spec.materials.len() <= 1); + + let model_material = match model_spec.find_first_material(&assets) { + MaterialSearchResult::Found(model_material_asset) => { + let Some(model_material) = assets.get(&model_material_asset) else { + return; + }; + + model_material + } + MaterialSearchResult::NotFound => { + return; + } + MaterialSearchResult::NoMaterials => &Material::builder().build(), + }; + + for texture_asset in model_material.textures() { + let Some(texture) = assets.get(texture_asset) else { + return; + }; + + let Texture::Texture2D(texture) = texture else { + tracing::error!( + texture_asset_id = ?texture_asset.id(), + texture_asset_label = ?assets.get_label(texture_asset), + "Material texture map is not 2D" + ); + return; + }; + + let texture_object_id = ObjectId::Asset(texture_asset.id()); + + if object_store.contains_maybe_pending_with_id(&texture_object_id) { + return; + } + + let Some(tex_pixel_data_format) = + TexturePixelDataFormat::for_image(&texture.image) + else { + tracing::error!("No texture pixel data format is available for image"); + return; + }; + + object_store.insert_pending(texture_object_id); + + render_pass.commands.push(Command::CreateTexture { + obj_id: texture_object_id, + pixel_data_format: tex_pixel_data_format, + creation: TextureCreation::Texture2D { + size: texture.image.dimensions(), + image: Some(texture.image.as_bytes().to_vec().into_boxed_slice()), + }, + properties: texture.properties.clone(), + }); + } + + if !object_store.contains_maybe_pending_with_id(&ObjectId::Asset(mesh_asset.id())) { + object_store.insert_pending(ObjectId::Asset(mesh_asset.id())); + + render_pass.commands.push(Command::CreateMesh { + obj_id: ObjectId::Asset(mesh_asset.id()), + mesh: AssetOrValue::Asset(mesh_asset.clone()), + usage: MeshUsage::Static, + }); + } +} + +fn add_set_3d_shader_point_light_bindings( + render_pass: &mut RenderPass, + point_light_query: &Query<( + &PointLight, + &Transform, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + )>, + main_3d_shader_obj_id: ObjectId, + main_3d_shader_cursor: &ShaderCursor<'_>, +) -> Result<(), crate::Error> +{ + let lighting_shader_cursor = + main_3d_shader_cursor.field("Uniforms").field("lighting"); + + render_pass + .commands + .reserve(1 + point_light_query.iter().count() * 6); + + render_pass.commands.push(Command::SetShaderBinding( + main_3d_shader_obj_id, + lighting_shader_cursor.field("point_light_cnt").binding( + u32::try_from(point_light_query.iter().count()) + .expect("Point light count does not fit in 32-bit unsigned integer") + .into(), + )?, + )); + + for (point_light_index, (point_light, point_light_transform)) in + point_light_query.iter().enumerate() + { + let point_light_shader_cursor = lighting_shader_cursor + .field("point_lights") + .element(point_light_index); + + let phong_shader_cursor = point_light_shader_cursor.field("phong"); + + let attenuation_props_shader_cursor = + point_light_shader_cursor.field("attenuation_props"); + + render_pass.commands.extend( + [ + phong_shader_cursor + .field("diffuse") + .binding(point_light.diffuse.to_rgb_lossy().into())?, + phong_shader_cursor + .field("specular") + .binding(point_light.specular.to_rgb_lossy().into())?, + point_light_shader_cursor.field("position").binding( + (point_light_transform.position + point_light.local_position).into(), + )?, + attenuation_props_shader_cursor + .field("constant") + .binding(point_light.attenuation_params.constant.into())?, + attenuation_props_shader_cursor + .field("linear") + .binding(point_light.attenuation_params.linear.into())?, + attenuation_props_shader_cursor + .field("quadratic") + .binding(point_light.attenuation_params.quadratic.into())?, + ] + .map(|binding| Command::SetShaderBinding(main_3d_shader_obj_id, binding)), + ); + } + + Ok(()) +} + +fn add_set_3d_shader_dir_lights_bindings( + render_pass: &mut RenderPass, + directional_light_query: &Query<( + &DirectionalLight, + Traverse<(With<Scene>, With<ActiveScene>), TraverseUp, ChildOf>, + )>, + main_3d_shader_obj_id: ObjectId, + main_3d_shader_cursor: &ShaderCursor<'_>, +) -> Result<(), crate::Error> +{ + let lighting_shader_cursor = + main_3d_shader_cursor.field("Uniforms").field("lighting"); + + render_pass + .commands + .reserve(1 + directional_light_query.iter().count() * 3); + + render_pass.commands.push(Command::SetShaderBinding( + main_3d_shader_obj_id, + lighting_shader_cursor + .field("directional_light_cnt") + .binding( + u32::try_from(directional_light_query.iter().count()) + .expect( + "Directional light count does not fit in 32-bit unsigned integer", + ) + .into(), + )?, + )); + + for (directional_light_index, (directional_light,)) in + directional_light_query.iter().enumerate() + { + let directional_light_shader_cursor = lighting_shader_cursor + .field("directional_lights") + .element(directional_light_index); + + let phong_shader_cursor = directional_light_shader_cursor.field("phong"); + + render_pass.commands.extend( + [ + phong_shader_cursor + .field("diffuse") + .binding(directional_light.diffuse.to_rgb_lossy().into())?, + phong_shader_cursor + .field("specular") + .binding(directional_light.specular.to_rgb_lossy().into())?, + directional_light_shader_cursor + .field("direction") + .binding(directional_light.direction.into())?, + ] + .map(|binding| Command::SetShaderBinding(main_3d_shader_obj_id, binding)), + ); + } + + Ok(()) +} + +fn add_set_3d_shader_camera_bindings( + render_pass: &mut RenderPass, + (camera, camera_transform): (&Camera, &Transform), + window: &Window, + main_3d_shader_obj_id: ObjectId, + main_3d_shader_cursor: &ShaderCursor<'_>, +) -> Result<(), crate::Error> +{ + let model_3d_shader_cursor = + main_3d_shader_cursor.field("Uniforms").field("model_3d"); + + let lighting_shader_cursor = + main_3d_shader_cursor.field("Uniforms").field("lighting"); + + render_pass.commands.extend( + [ + model_3d_shader_cursor + .field("view") + .binding(camera.to_view_matrix(camera_transform.position).into())?, + model_3d_shader_cursor.field("projection").binding( + camera + .projection + .to_matrix_rh(window.inner_size, ProjectionClipVolume::NegOneToOne) + .into(), + )?, + lighting_shader_cursor + .field("view_pos") + .binding(camera_transform.position.into())?, + ] + .into_iter() + .map(|binding| Command::SetShaderBinding(main_3d_shader_obj_id, binding)), + ); + + Ok(()) +} + +fn add_set_3d_shader_renderable_bindings( + render_pass: &mut RenderPass, + renderable: (&Material, Option<&MaterialFlags>, Option<&Transform>), + scene_env_lighting: Option<&EnvironmentalLighting>, + main_3d_shader_obj_id: ObjectId, + main_3d_shader_cursor: &ShaderCursor<'_>, + white_1x1_tex_obj_id: ObjectId, +) -> Result<(), crate::Error> +{ + let (material, material_flags, transform) = renderable; + + let transform = match transform.as_deref() { + Some(transform) => transform, + None => &Transform::default(), + }; + + let model_matrix = transform.to_matrix(); + let inverted_model_matrix = model_matrix.inverse(); + + let material_flags = material_flags + .as_deref() + .unwrap_or(&const { MaterialFlags::builder().build() }); + + let env_lighting = match &scene_env_lighting { + Some(env_lighting) => &env_lighting, + None => &EnvironmentalLighting::default(), + }; + + let model_3d_shader_cursor = + main_3d_shader_cursor.field("Uniforms").field("model_3d"); + + let material_shader_cursor = main_3d_shader_cursor + .field("Uniforms") + .field("lighting") + .field("material"); + + let diffuse_map_obj_id = material + .diffuse_map + .as_ref() + .map(|diffuse_map| ObjectId::Asset(diffuse_map.id())) + .unwrap_or(white_1x1_tex_obj_id); + + render_pass.commands.extend( + [ + model_3d_shader_cursor + .field("model") + .binding(model_matrix.into())?, + model_3d_shader_cursor + .field("model_inverted") + .binding(inverted_model_matrix.into())?, + material_shader_cursor.field("ambient").binding( + material_flags + .use_ambient_color + .then_some(&material.ambient) + .unwrap_or(&env_lighting.ambient_color) + .to_rgb_lossy() + .into(), + )?, + material_shader_cursor + .field("diffuse") + .binding(material.diffuse.to_rgb_lossy().into())?, + material_shader_cursor + .field("specular") + .binding(material.specular.to_rgb_lossy().into())?, + main_3d_shader_cursor.field("ambient_map").binding( + ShaderBindingValue::Texture( + material + .ambient_map + .as_ref() + .map(|ambient_map| ObjectId::Asset(ambient_map.id())) + .unwrap_or(diffuse_map_obj_id), + ShaderBindingTextureKind::Texture2D, + ), + )?, + main_3d_shader_cursor.field("diffuse_map").binding( + ShaderBindingValue::Texture( + diffuse_map_obj_id, + ShaderBindingTextureKind::Texture2D, + ), + )?, + main_3d_shader_cursor.field("specular_map").binding( + ShaderBindingValue::Texture( + material + .specular_map + .as_ref() + .map(|specular_map| ObjectId::Asset(specular_map.id())) + .unwrap_or(white_1x1_tex_obj_id), + ShaderBindingTextureKind::Texture2D, + ), + )?, + material_shader_cursor + .field("shininess") + .binding(material.shininess.into())?, + ] + .into_iter() + .map(|binding| Command::SetShaderBinding(main_3d_shader_obj_id, binding)), + ); + + Ok(()) +} + +fn add_sky_box_commands( + render_pass: &mut RenderPass, + sky_box_ids: &SkyBoxIds, + camera: &Camera, + camera_transform: &Transform, + window: &Window, + sky_box_shader_obj_id: ObjectId, + sky_box_shader_program: &ShaderProgram, +) -> Result<(), crate::Error> +{ + render_pass + .commands + .push(Command::ActivateShader(sky_box_shader_obj_id)); + + let sky_box_shader_cursor = ShaderCursor::new( + sky_box_shader_program + .reflection(0) + .unwrap() + .global_params_var_layout() + .unwrap(), + ); + + let mut view = camera.to_view_matrix(camera_transform.position); + + view.translate(&Vec3 { x: 0.0, y: 0.0, z: 0.0 }); + + let sky_box_shader_bindings = [ + sky_box_shader_cursor + .field("Uniforms") + .field("projection") + .binding( + (match camera.projection { + Projection::Perspective(_) => camera.projection.to_matrix_rh( + window.inner_size, + ProjectionClipVolume::NegOneToOne, + ), + Projection::Orthographic(_) => { + // A orthographic projection cannot be used for a sky box + Perspective::default().to_matrix_rh( + window.inner_size.width as f32 + / window.inner_size.height as f32, + ProjectionClipVolume::NegOneToOne, + ) + } + }) + .into(), + )?, + sky_box_shader_cursor + .field("Uniforms") + .field("view") + .binding(view.into())?, + sky_box_shader_cursor.field("cube_texture").binding( + ShaderBindingValue::Texture( + sky_box_ids.texture_object, + ShaderBindingTextureKind::Cube, + ), + )?, + ]; + + for binding in sky_box_shader_bindings { + render_pass + .commands + .push(Command::SetShaderBinding(sky_box_shader_obj_id, binding)); + } + + render_pass.commands.push(Command::UpdateDrawProperties( + DrawProperties { + depth_function: DepthFunction::LessOrEqual, + ..Default::default() + }, + DrawPropertiesUpdateFlags::DEPTH_FUNCTION, + )); + + render_pass.commands.push(Command::DrawMesh( + sky_box_ids.mesh_object, + DrawMeshOptions::default(), + )); + + Ok(()) +} + +fn load_sky_box( + scene_ent_id: Uid, + sky_box: &SkyBox, + sky_box_state: Option<&SkyBoxState>, + assets: &mut Assets, + object_store: &mut ObjectStore, + render_pass: &mut RenderPass, + actions: &mut Actions, +) -> Result<Option<SkyBoxIds>, crate::Error> +{ + let mesh_object_id = match &sky_box_state { + Some(sky_box_state) => sky_box_state.mesh_object_id, + None => { + let sky_box_mesh = + Mesh::cube(Dimens3 { width: 1.0, height: 1.0, depth: 1.0 }); + + let mesh_object_id = ObjectId::new_sequential(); + + object_store.insert_pending(mesh_object_id); + + render_pass.commands.push(Command::CreateMesh { + obj_id: mesh_object_id, + mesh: AssetOrValue::Value(sky_box_mesh), + usage: MeshUsage::Static, + }); + + mesh_object_id + } + }; + + let texture_object_id = match &sky_box { + SkyBox::AssetPerCubeMapFace(cube_map_part_assets) => { + if let Some(sky_box_state) = sky_box_state { + sky_box_state.texture_object_id + } else { + let Some(part_images) = cube_map_part_assets + .iter() + .map_while(|(face, part_texture_asset)| { + let part_texture = assets.get(part_texture_asset)?; + + match part_texture { + Texture::Texture2D(part_texture) => { + Some((*face, &part_texture.image)) + } + Texture::CubeMap(_) => { + tracing::warn!( + "Cube map texture cannot be used as cube map part" + ); + + None + } + } + }) + .collect::<Vec<_>>() + .as_array::<6>() + .cloned() + else { + return Ok(None); + }; + + let texture_object_id = ObjectId::new_sequential(); + + object_store.insert_pending(texture_object_id); + + render_pass.commands.push(Command::CreateTexture { + obj_id: texture_object_id, + pixel_data_format: TexturePixelDataFormat::for_image( + part_images[0].1, + ) + .expect("No texture pixel data format is available for image"), + creation: TextureCreation::CubeMap { + size: part_images[0].1.dimensions(), + images: Some(part_images.map(|(face, image)| { + (face, image.as_bytes().to_vec().into_boxed_slice()) + })), + }, + properties: TextureProperties::builder() + .minifying_filter(TextureFiltering::Linear) + .magnifying_filter(TextureFiltering::Linear) + .wrap(TextureWrapping::ClampToEdge) + .build(), + }); + + texture_object_id + } + } + }; + + if sky_box_state.is_none() { + actions.add_components( + scene_ent_id, + (SkyBoxState { texture_object_id, mesh_object_id },), + ); + } + + Ok(Some(SkyBoxIds { + texture_object: texture_object_id, + mesh_object: mesh_object_id, + })) +} + +#[derive(Debug, Component)] +pub struct SkyBoxState +{ + texture_object_id: ObjectId, + mesh_object_id: ObjectId, +} diff --git a/engine/src/rendering/object.rs b/engine/src/rendering/object.rs new file mode 100644 index 0000000..61e7ed5 --- /dev/null +++ b/engine/src/rendering/object.rs @@ -0,0 +1,137 @@ +use std::collections::HashMap; +use std::fmt::Display; +use std::sync::atomic::{AtomicU64, Ordering}; + +use crate::asset::Id as AssetId; +use crate::ecs::Sole; + +pub type RawValue = u32; + +/// Rendering object ID. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Id +{ + Asset(AssetId), + Sequential(SequentialId), +} + +impl Id +{ + pub fn new_sequential() -> Self + { + static NEXT_SEQUENTIAL_ID: AtomicU64 = AtomicU64::new(0); + + Self::Sequential(SequentialId( + NEXT_SEQUENTIAL_ID.fetch_add(1, Ordering::Relaxed), + )) + } + + pub fn into_asset_id(self) -> Option<AssetId> + { + match self { + Self::Asset(asset_id) => Some(asset_id), + Self::Sequential(_) => None, + } + } +} + +impl From<AssetId> for Id +{ + fn from(asset_id: AssetId) -> Self + { + Self::Asset(asset_id) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SequentialId(u64); + +/// Rendering object store. +#[derive(Debug, Default, Sole)] +pub struct Store +{ + objects: HashMap<Id, Option<Object>>, +} + +impl Store +{ + pub fn get_obj(&self, id: &Id) -> Option<&Object> + { + self.objects.get(id).and_then(|obj| obj.as_ref()) + } + + pub fn contains_maybe_pending_with_id(&self, id: &Id) -> bool + { + self.objects.contains_key(id) + } + + pub fn contains_non_pending_with_id(&self, id: &Id) -> bool + { + self.objects.get(id).and_then(|obj| obj.as_ref()).is_some() + } + + pub fn insert(&mut self, id: Id, object: Object) + { + self.objects.insert(id, Some(object)); + } + + pub fn insert_pending(&mut self, id: Id) + { + self.objects.insert(id, None); + } + + pub fn remove(&mut self, id: &Id) -> Option<Option<Object>> + { + self.objects.remove(id) + } +} + +/// Rendering object. +#[derive(Debug, Clone)] +pub struct Object +{ + raw: RawValue, + kind: Kind, +} + +impl Object +{ + pub fn from_raw(raw: RawValue, kind: Kind) -> Self + { + Self { raw, kind } + } + + pub fn as_raw(&self) -> RawValue + { + self.raw + } + + pub fn kind(&self) -> Kind + { + self.kind + } +} + +/// Rendering object kind. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum Kind +{ + Texture, + ShaderProgram, + Mesh, + Framebuffer, +} + +impl Display for Kind +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + formatter.write_str(match self { + Self::Texture => "texture", + Self::ShaderProgram => "shader program", + Self::Mesh => "mesh", + Self::Framebuffer => "framebuffer", + }) + } +} diff --git a/engine/src/rendering/shader.rs b/engine/src/rendering/shader.rs new file mode 100644 index 0000000..3f07381 --- /dev/null +++ b/engine/src/rendering/shader.rs @@ -0,0 +1,1404 @@ +use std::any::type_name; +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; +use std::fmt::{Debug, Display, Write}; +use std::path::Path; +use std::str::Utf8Error; +use std::sync::Arc; + +use bitflags::{bitflags, bitflags_match}; +use ecs::phase::INIT as INIT_PHASE; +use shader_slang::{ + Blob as SlangBlob, + ComponentType as SlangComponentType, + DebugInfoLevel as SlangDebugInfoLevel, + EntryPoint as SlangEntryPoint, + GlobalSession as SlangGlobalSession, + Module as SlangModule, + ParameterCategory as SlangParameterCategory, + ScalarType as SlangScalarType, + Session as SlangSession, + TypeKind as SlangTypeKind, +}; + +use crate::asset::{ + Assets, + Event as AssetEvent, + Handle as AssetHandle, + Id as AssetId, + Submitter as AssetSubmitter, + HANDLE_ASSETS_PHASE, +}; +use crate::builder; +use crate::ecs::pair::ChildOf; +use crate::ecs::phase::Phase; +use crate::ecs::sole::Single; +use crate::ecs::{declare_entity, pair, Sole}; +use crate::mesh::vertex_buffer::VertexLabel; + +pub mod cursor; + +pub const STD_VERT_IN_SEM_NAME_POSITION: &str = "STD_POSITION"; +pub const STD_VERT_IN_SEM_NAME_NORMAL: &str = "STD_NORMAL"; +pub const STD_VERT_IN_SEM_NAME_UV_FROM_TOP_LEFT: &str = "STD_UV_FROM_TOP_LEFT"; +pub const STD_VERT_IN_SEM_NAME_COLOR: &str = "STD_COLOR"; + +/// Shader module. +#[derive(Debug)] +pub struct ModuleSource +{ + pub name: Cow<'static, str>, + pub file_path: Cow<'static, Path>, + pub source: Cow<'static, str>, + pub link_entrypoints: EntrypointFlags, +} + +bitflags! { + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] + pub struct EntrypointFlags: usize + { + const FRAGMENT = 1 << 0; + const VERTEX = 1 << 1; + } +} + +#[derive(Clone)] +pub struct Module +{ + inner: SlangModule, +} + +impl Module +{ + pub fn entry_points(&self) -> impl ExactSizeIterator<Item = EntryPoint> + use<'_> + { + self.inner + .entry_points() + .map(|entry_point| EntryPoint { inner: entry_point }) + } + + pub fn get_entry_point(&self, entry_point: &str) -> Option<EntryPoint> + { + let entry_point = self.inner.find_entry_point_by_name(entry_point)?; + + Some(EntryPoint { inner: entry_point }) + } + + pub fn file_path(&self) -> &str + { + self.inner.file_path() + } +} + +pub struct EntryPoint +{ + inner: SlangEntryPoint, +} + +impl EntryPoint +{ + pub fn function(&self) -> FunctionReflection<'_> + { + FunctionReflection { + inner: self.inner.function_reflection(), + } + } +} + +pub struct FunctionReflection<'a> +{ + inner: &'a shader_slang::reflection::Function, +} + +impl<'a> FunctionReflection<'a> +{ + pub fn name(&self) -> Option<&str> + { + self.inner.name() + } +} + +pub struct EntryPointReflection<'a> +{ + inner: &'a shader_slang::reflection::EntryPoint, +} + +impl<'a> EntryPointReflection<'a> +{ + pub fn name(&self) -> Option<&str> + { + self.inner.name() + } + + pub fn name_override(&self) -> Option<&str> + { + self.inner.name_override() + } + + pub fn stage(&self) -> Stage + { + Stage::from_slang_stage(self.inner.stage()) + } + + pub fn parameters(&self) -> impl ExactSizeIterator<Item = VariableLayout<'a>> + { + self.inner + .parameters() + .map(|param| VariableLayout { inner: param }) + } + + pub fn var_layout(&self) -> Option<VariableLayout<'a>> + { + Some(VariableLayout { inner: self.inner.var_layout()? }) + } +} + +#[derive(Clone)] +pub struct Program +{ + inner: SlangComponentType, + metadata: ProgramMetadata, +} + +impl Program +{ + pub fn into_linked(self) -> Result<Program, Error> + { + let linked_program = self.inner.link()?; + + Ok(Program { + inner: linked_program, + metadata: self.metadata, + }) + } + + pub fn metadata(&self) -> &ProgramMetadata + { + &self.metadata + } + + pub fn get_entry_point_code(&self, entry_point_index: u32) -> Result<Blob, Error> + { + let blob = self.inner.entry_point_code(entry_point_index.into(), 0)?; + + Ok(Blob { inner: blob }) + } + + pub fn reflection(&self, target: u32) -> Result<ProgramReflection<'_>, Error> + { + let reflection = self.inner.layout(target as i64)?; + + Ok(ProgramReflection { inner: reflection }) + } +} + +impl Debug for Program +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + formatter + .debug_struct(type_name::<Self>()) + .finish_non_exhaustive() + } +} + +pub struct ProgramReflection<'a> +{ + inner: &'a shader_slang::reflection::Shader, +} + +impl<'a> ProgramReflection<'a> +{ + pub fn get_entry_point_by_index(&self, index: u32) + -> Option<EntryPointReflection<'a>> + { + Some(EntryPointReflection { + inner: self.inner.entry_point_by_index(index)?, + }) + } + + pub fn get_entry_point_by_name(&self, name: &str) + -> Option<EntryPointReflection<'a>> + { + Some(EntryPointReflection { + inner: self.inner.find_entry_point_by_name(name)?, + }) + } + + pub fn entry_points( + &self, + ) -> impl ExactSizeIterator<Item = EntryPointReflection<'a>> + use<'a> + { + self.inner + .entry_points() + .map(|entry_point| EntryPointReflection { inner: entry_point }) + } + + pub fn global_params_type_layout(&self) -> Option<TypeLayout<'a>> + { + Some(TypeLayout { + inner: self.inner.global_params_type_layout()?, + }) + } + + pub fn global_params_var_layout(&self) -> Option<VariableLayout<'a>> + { + Some(VariableLayout { + inner: self.inner.global_params_var_layout()?, + }) + } + + pub fn get_type(&self, name: &str) -> Option<TypeReflection<'a>> + { + Some(TypeReflection { + inner: self.inner.find_type_by_name(name)?, + }) + } + + pub fn get_type_layout(&self, ty: &TypeReflection<'a>) -> Option<TypeLayout<'a>> + { + Some(TypeLayout { + inner: self + .inner + .type_layout(&ty.inner, shader_slang::LayoutRules::Default)?, + }) + } +} + +#[derive(Clone, Copy)] +pub struct VariableLayout<'a> +{ + inner: &'a shader_slang::reflection::VariableLayout, +} + +impl<'a> VariableLayout<'a> +{ + pub fn name(&self) -> Option<&'a str> + { + self.inner.name() + } + + pub fn semantic_name(&self) -> Option<&str> + { + self.inner.semantic_name() + } + + pub fn binding_index(&self) -> u32 + { + self.inner + .offset(shader_slang::ParameterCategory::DescriptorTableSlot) as u32 + + // self.inner.binding_index() + } + + pub fn varying_input_offset(&self) -> Option<usize> + { + if !self + .inner + .categories() + .any(|category| category == SlangParameterCategory::VaryingInput) + { + return None; + } + + Some(self.inner.offset(SlangParameterCategory::VaryingInput)) + } + + pub fn binding_space(&self) -> u32 + { + self.inner.binding_space() + } + + pub fn semantic_index(&self) -> usize + { + self.inner.semantic_index() + } + + pub fn offset(&self) -> usize + { + self.inner.offset(shader_slang::ParameterCategory::Uniform) + } + + pub fn ty(&self) -> Option<TypeReflection<'a>> + { + self.inner.ty().map(|ty| TypeReflection { inner: ty }) + } + + pub fn type_layout(&self) -> Option<TypeLayout<'a>> + { + Some(TypeLayout { inner: self.inner.type_layout()? }) + } +} + +#[derive(Clone, Copy)] +pub struct TypeLayout<'a> +{ + inner: &'a shader_slang::reflection::TypeLayout, +} + +impl<'a> TypeLayout<'a> +{ + pub fn kind(&self) -> TypeKind + { + TypeKind::from_slang_type_kind(self.inner.kind()) + } + + pub fn scalar_type(&self) -> Option<ScalarType> + { + Some(ScalarType::from_slang_scalar_type( + self.inner.scalar_type()?, + )) + } + + pub fn resource_shape(&self) -> Option<ResourceShape> + { + Some(ResourceShape::from_bits_retain( + self.inner.resource_shape()? as u32, + )) + } + + pub fn get_field_by_name(&self, name: &str) -> Option<VariableLayout<'a>> + { + let index = self.inner.find_field_index_by_name(name); + + if index < 0 { + return None; + } + + let index = u32::try_from(index.cast_unsigned()).expect("Should not happend"); + + let field = self.inner.field_by_index(index)?; + + Some(VariableLayout { inner: field }) + } + + pub fn parameter_category(&self) -> ParameterCategory + { + ParameterCategory::from_slang_parameter_category(self.inner.parameter_category()) + } + + pub fn binding_range_descriptor_set_index(&self, index: i64) -> i64 + { + self.inner.binding_range_descriptor_set_index(index) + } + + pub fn get_field_binding_range_offset_by_name(&self, name: &str) -> Option<u64> + { + let field_index = self.inner.find_field_index_by_name(name); + + if field_index < 0 { + return None; + } + + let field_binding_range_offset = + self.inner.field_binding_range_offset(field_index); + + if field_binding_range_offset < 0 { + return None; + } + + Some(field_binding_range_offset.cast_unsigned()) + } + + pub fn ty(&self) -> Option<TypeReflection<'a>> + { + self.inner.ty().map(|ty| TypeReflection { inner: ty }) + } + + pub fn fields(&self) -> FieldIter<'a> + { + FieldIter { + type_layout: self.clone(), + cnt: self.field_cnt(), + index: 0, + } + } + + pub fn field_cnt(&self) -> u32 + { + self.inner.field_count() + } + + pub fn element_cnt(&self) -> Option<usize> + { + self.inner.element_count() + } + + pub fn row_cnt(&self) -> Option<u32> + { + self.inner.row_count() + } + + pub fn column_cnt(&self) -> Option<u32> + { + self.inner.column_count() + } + + pub fn element_type_layout(&self) -> Option<TypeLayout<'a>> + { + self.inner + .element_type_layout() + .map(|type_layout| TypeLayout { inner: type_layout }) + } + + pub fn element_var_layout(&self) -> Option<VariableLayout<'a>> + { + self.inner + .element_var_layout() + .map(|var_layout| VariableLayout { inner: var_layout }) + } + + pub fn container_var_layout(&self) -> Option<VariableLayout<'a>> + { + self.inner + .container_var_layout() + .map(|var_layout| VariableLayout { inner: var_layout }) + } + + pub fn uniform_size(&self) -> Option<usize> + { + // tracing::debug!( + // "uniform_size: {:?} categories: {:?}", + // self.inner.name(), + // self.inner.categories().collect::<Vec<_>>(), + // ); + + if !self + .inner + .categories() + .any(|category| category == SlangParameterCategory::Uniform) + { + return None; + } + + // let category = self.inner.categories().next().unwrap(); + + // println!( + // "AARGH size Category: {category:?} Category count: {}", + // self.inner.category_count() + // ); + + // Some(self.inner.size(category)) + + Some(self.inner.size(SlangParameterCategory::Uniform)) + } + + pub fn stride(&self) -> usize + { + self.inner.stride(self.inner.categories().next().unwrap()) + } +} + +pub struct FieldIter<'a> +{ + type_layout: TypeLayout<'a>, + cnt: u32, + index: u32, +} + +impl<'a> Iterator for FieldIter<'a> +{ + type Item = VariableLayout<'a>; + + fn next(&mut self) -> Option<Self::Item> + { + if self.index == self.cnt { + return None; + } + + let Some(field) = self.type_layout.inner.field_by_index(self.index) else { + unreachable!(); + }; + + self.index += 1; + + Some(VariableLayout { inner: field }) + } + + fn size_hint(&self) -> (usize, Option<usize>) + { + let len = (self.cnt - self.index) as usize; + + (len, Some(len)) + } +} + +impl ExactSizeIterator for FieldIter<'_> +{ + fn len(&self) -> usize + { + (self.cnt - self.index) as usize + } +} + +impl DoubleEndedIterator for FieldIter<'_> +{ + fn next_back(&mut self) -> Option<Self::Item> + { + if self.cnt == 0 || self.index == self.cnt - 1 { + return None; + } + + let Some(field) = self.type_layout.inner.field_by_index(self.cnt - 1) else { + unreachable!(); + }; + + self.cnt -= 1; + + Some(VariableLayout { inner: field }) + } +} + +pub struct TypeReflection<'a> +{ + inner: &'a shader_slang::reflection::Type, +} + +impl TypeReflection<'_> +{ + pub fn kind(&self) -> TypeKind + { + TypeKind::from_slang_type_kind(self.inner.kind()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum TypeKind +{ + None, + Struct, + Enum, + Array, + Matrix, + Vector, + Scalar, + ConstantBuffer, + Resource, + SamplerState, + TextureBuffer, + ShaderStorageBuffer, + ParameterBlock, + GenericTypeParameter, + Interface, + OutputStream, + MeshOutput, + Specialized, + Feedback, + Pointer, + DynamicResource, + Count, +} + +impl TypeKind +{ + fn from_slang_type_kind(type_kind: SlangTypeKind) -> Self + { + match type_kind { + SlangTypeKind::None => Self::None, + SlangTypeKind::Struct => Self::Struct, + SlangTypeKind::Enum => Self::Enum, + SlangTypeKind::Array => Self::Array, + SlangTypeKind::Matrix => Self::Matrix, + SlangTypeKind::Vector => Self::Vector, + SlangTypeKind::Scalar => Self::Scalar, + SlangTypeKind::ConstantBuffer => Self::ConstantBuffer, + SlangTypeKind::Resource => Self::Resource, + SlangTypeKind::SamplerState => Self::SamplerState, + SlangTypeKind::TextureBuffer => Self::TextureBuffer, + SlangTypeKind::ShaderStorageBuffer => Self::ShaderStorageBuffer, + SlangTypeKind::ParameterBlock => Self::ParameterBlock, + SlangTypeKind::GenericTypeParameter => Self::GenericTypeParameter, + SlangTypeKind::Interface => Self::Interface, + SlangTypeKind::OutputStream => Self::OutputStream, + SlangTypeKind::MeshOutput => Self::MeshOutput, + SlangTypeKind::Specialized => Self::Specialized, + SlangTypeKind::Feedback => Self::Feedback, + SlangTypeKind::Pointer => Self::Pointer, + SlangTypeKind::DynamicResource => Self::DynamicResource, + SlangTypeKind::Count => Self::Count, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[non_exhaustive] +pub enum ScalarType +{ + None, + Void, + Bool, + Int32, + Uint32, + Int64, + Uint64, + Float16, + Float32, + Float64, + Int8, + Uint8, + Int16, + Uint16, + Intptr, + Uintptr, + Bfloat16, + FloatE4m3, + FloatE5m2, +} + +impl ScalarType +{ + fn from_slang_scalar_type(scalar_type: SlangScalarType) -> Self + { + match scalar_type { + SlangScalarType::None => Self::None, + SlangScalarType::Void => Self::Void, + SlangScalarType::Bool => Self::Bool, + SlangScalarType::Int32 => Self::Int32, + SlangScalarType::Uint32 => Self::Uint32, + SlangScalarType::Int64 => Self::Int64, + SlangScalarType::Uint64 => Self::Uint64, + SlangScalarType::Float16 => Self::Float16, + SlangScalarType::Float32 => Self::Float32, + SlangScalarType::Float64 => Self::Float64, + SlangScalarType::Int8 => Self::Int8, + SlangScalarType::Uint8 => Self::Uint8, + SlangScalarType::Int16 => Self::Int16, + SlangScalarType::Uint16 => Self::Uint16, + SlangScalarType::Intptr => Self::Intptr, + SlangScalarType::Uintptr => Self::Uintptr, + SlangScalarType::Bfloat16 => Self::Bfloat16, + SlangScalarType::FloatE4m3 => Self::FloatE4m3, + SlangScalarType::FloatE5m2 => Self::FloatE5m2, + #[allow(unreachable_patterns)] + _ => unimplemented!("conversion from slang scalar type"), + } + } +} + +bitflags! { +#[derive(Debug, Copy, Clone)] +pub struct ResourceShape: u32 { + const BASE = shader_slang::ResourceShape::SlangResourceBaseShapeMask as u32; + const NONE = shader_slang::ResourceShape::SlangResourceNone as u32; + const TEXTURE_1D = shader_slang::ResourceShape::SlangTexture1d as u32; + const TEXTURE_2D = shader_slang::ResourceShape::SlangTexture2d as u32; + const TEXTURE_3D = shader_slang::ResourceShape::SlangTexture3d as u32; + const TEXTURE_CUBE = shader_slang::ResourceShape::SlangTextureCube as u32; + const TEXTURE_BUFFER = shader_slang::ResourceShape::SlangTextureBuffer as u32; + const STRUCTURED_BUFFER = shader_slang::ResourceShape::SlangStructuredBuffer as u32; + const BYTE_ADDRESS_BUFFER = shader_slang::ResourceShape::SlangByteAddressBuffer as u32; + const UNKNOWN = shader_slang::ResourceShape::SlangResourceUnknown as u32; + const ACCELERATION_STRUCTURE = shader_slang::ResourceShape::SlangAccelerationStructure as u32; + const TEXTURE_SUBPASS = shader_slang::ResourceShape::SlangTextureSubpass as u32; + const EXT_SHAPE = shader_slang::ResourceShape::SlangResourceExtShapeMask as u32; + const TEXTURE_FEEDBACK_FLAG = shader_slang::ResourceShape::SlangTextureFeedbackFlag as u32; + const TEXTURE_SHADOW_FLAG = shader_slang::ResourceShape::SlangTextureShadowFlag as u32; + const TEXTURE_ARRAY_FLAG = shader_slang::ResourceShape::SlangTextureArrayFlag as u32; + const TEXTURE_MULTISAMPLE_FLAG = shader_slang::ResourceShape::SlangTextureMultisampleFlag as u32; + const TEXTURE_COMBINED_FLAG = shader_slang::ResourceShape::SlangTextureCombinedFlag as u32; + const TEXTURE_1D_ARRAY = shader_slang::ResourceShape::SlangTexture1dArray as u32; + const TEXTURE_2D_ARRAY = shader_slang::ResourceShape::SlangTexture2dArray as u32; + const TEXTURE_CUBE_ARRAY = shader_slang::ResourceShape::SlangTextureCubeArray as u32; + const TEXTURE_2D_MULTISAMPLE = shader_slang::ResourceShape::SlangTexture2dMultisample as u32; + const TEXTURE_2D_MULTISAMPLE_ARRAY = shader_slang::ResourceShape::SlangTexture2dMultisampleArray as u32; + const TEXTURE_SUBPASS_MULTISAMPLE = shader_slang::ResourceShape::SlangTextureSubpassMultisample as u32; +} +} + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub enum ParameterCategory +{ + None, + Mixed, + ConstantBuffer, + ShaderResource, + UnorderedAccess, + VaryingInput, + VaryingOutput, + SamplerState, + Uniform, + DescriptorTableSlot, + SpecializationConstant, + PushConstantBuffer, + RegisterSpace, + Generic, + RayPayload, + HitAttributes, + CallablePayload, + ShaderRecord, + ExistentialTypeParam, + ExistentialObjectParam, + SubElementRegisterSpace, + Subpass, + MetalArgumentBufferElement, + MetalAttribute, + MetalPayload, + Count, +} + +impl ParameterCategory +{ + fn from_slang_parameter_category(parameter_category: SlangParameterCategory) -> Self + { + match parameter_category { + SlangParameterCategory::None => Self::None, + SlangParameterCategory::Mixed => Self::Mixed, + SlangParameterCategory::ConstantBuffer => Self::ConstantBuffer, + SlangParameterCategory::ShaderResource => Self::ShaderResource, + SlangParameterCategory::UnorderedAccess => Self::UnorderedAccess, + SlangParameterCategory::VaryingInput => Self::VaryingInput, + SlangParameterCategory::VaryingOutput => Self::VaryingOutput, + SlangParameterCategory::SamplerState => Self::SamplerState, + SlangParameterCategory::Uniform => Self::Uniform, + SlangParameterCategory::DescriptorTableSlot => Self::DescriptorTableSlot, + SlangParameterCategory::SpecializationConstant => { + Self::SpecializationConstant + } + SlangParameterCategory::PushConstantBuffer => Self::PushConstantBuffer, + SlangParameterCategory::RegisterSpace => Self::RegisterSpace, + SlangParameterCategory::Generic => Self::Generic, + SlangParameterCategory::RayPayload => Self::RayPayload, + SlangParameterCategory::HitAttributes => Self::HitAttributes, + SlangParameterCategory::CallablePayload => Self::CallablePayload, + SlangParameterCategory::ShaderRecord => Self::ShaderRecord, + SlangParameterCategory::ExistentialTypeParam => Self::ExistentialTypeParam, + SlangParameterCategory::ExistentialObjectParam => { + Self::ExistentialObjectParam + } + SlangParameterCategory::SubElementRegisterSpace => { + Self::SubElementRegisterSpace + } + SlangParameterCategory::Subpass => Self::Subpass, + SlangParameterCategory::MetalArgumentBufferElement => { + Self::MetalArgumentBufferElement + } + SlangParameterCategory::MetalAttribute => Self::MetalAttribute, + SlangParameterCategory::MetalPayload => Self::MetalPayload, + SlangParameterCategory::Count => Self::Count, + } + } +} + +pub struct Blob +{ + inner: SlangBlob, +} + +impl Blob +{ + pub fn as_bytes(&self) -> &[u8] + { + self.inner.as_slice() + } + + pub fn as_str(&self) -> Result<&str, Utf8Error> + { + self.inner.as_str() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum Stage +{ + None, + Vertex, + Hull, + Domain, + Geometry, + Fragment, + Compute, + RayGeneration, + Intersection, + AnyHit, + ClosestHit, + Miss, + Callable, + Mesh, + Amplification, + Dispatch, + Count, +} + +impl Stage +{ + fn from_slang_stage(stage: shader_slang::Stage) -> Self + { + match stage { + shader_slang::Stage::None => Self::None, + shader_slang::Stage::Vertex => Self::Vertex, + shader_slang::Stage::Hull => Self::Hull, + shader_slang::Stage::Domain => Self::Domain, + shader_slang::Stage::Geometry => Self::Geometry, + shader_slang::Stage::Fragment => Self::Fragment, + shader_slang::Stage::Compute => Self::Compute, + shader_slang::Stage::RayGeneration => Self::RayGeneration, + shader_slang::Stage::Intersection => Self::Intersection, + shader_slang::Stage::AnyHit => Self::AnyHit, + shader_slang::Stage::ClosestHit => Self::ClosestHit, + shader_slang::Stage::Miss => Self::Miss, + shader_slang::Stage::Callable => Self::Callable, + shader_slang::Stage::Mesh => Self::Mesh, + shader_slang::Stage::Amplification => Self::Amplification, + shader_slang::Stage::Dispatch => Self::Dispatch, + shader_slang::Stage::Count => Self::Count, + } + } +} + +builder! { +#[builder(name = SettingsBuilder, derives=(Debug))] +#[derive(Debug)] +#[non_exhaustive] +pub struct Settings +{ + link_entrypoints: EntrypointFlags, +} +} + +#[derive(Sole)] +pub struct Context +{ + _global_session: SlangGlobalSession, + session: SlangSession, + modules: HashMap<AssetId, Module>, + programs: HashMap<AssetId, Program>, +} + +impl Context +{ + pub fn get_module(&self, asset_id: &AssetId) -> Option<&Module> + { + self.modules.get(asset_id) + } + + pub fn get_program(&self, asset_id: &AssetId) -> Option<&Program> + { + self.programs.get(asset_id) + } + + #[tracing::instrument(skip_all, fields(module_file = module.file_path()))] + pub fn compose_into_program( + &self, + module: Module, + link_entrypoints: EntrypointFlags, + ) -> Result<Program, ComposeProgramError> + { + let entry_points = link_entrypoints + .iter() + .filter_map(|entrypoint_flag| { + let entrypoint_name = bitflags_match!(entrypoint_flag, { + EntrypointFlags::VERTEX => Some("vertex_main"), + EntrypointFlags::FRAGMENT => Some("fragment_main"), + _ => None + })?; + + let Some(entry_point) = module.get_entry_point(entrypoint_name) else { + return Some(Err(ComposeProgramError::EntrypointNotFoundError { + entrypoint_name, + })); + }; + + Some(Ok(entry_point)) + }) + .collect::<Result<Vec<_>, _>>()?; + + let components = entry_points + .into_iter() + .map(|entry_point| SlangComponentType::from(entry_point.inner)) + .chain([SlangComponentType::from(module.inner)]) + .collect::<Vec<_>>(); + + let program = self + .session + .create_composite_component_type(&components) + .map_err(|err| ComposeProgramError::Other(Error(err)))?; + + let vertex_desc = if link_entrypoints.contains(EntrypointFlags::VERTEX) { + // TODO: Do not have hard coded target here + let Ok(program_reflection) = program.layout(0) else { + unreachable!(); + }; + + let program_reflection = ProgramReflection { inner: program_reflection }; + + Some(VertexDescription::new( + &program_reflection + .get_entry_point_by_name("vertex_main") + .expect("Not possible"), + )?) + } else { + None + }; + + Ok(Program { + inner: program, + metadata: ProgramMetadata { vertex_desc }, + }) + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct ProgramMetadata +{ + /// If the program has a entry point in the vertex stage, this field will contain a + /// description of the vertex type passed to the entry point. + pub vertex_desc: Option<VertexDescription>, +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct VertexDescription +{ + pub inputs: Arc<[VertexInputDescription]>, +} + +impl VertexDescription +{ + #[tracing::instrument(skip_all, fields(vs_entry_point_name = vs_entrypoint.name()))] + pub fn new( + vs_entrypoint: &EntryPointReflection<'_>, + ) -> Result<Self, VertexDescriptionError> + { + if vs_entrypoint.stage() != Stage::Vertex { + return Err(VertexDescriptionError::EntrypointNotInVertexStage); + } + + let inputs = VarInputDfsIter::new(vs_entrypoint).inspect(|var_input| { + if var_input.type_layout.kind() != TypeKind::Struct { + return; + } + + let Some(semantic_name) = var_input.var_layout.semantic_name() else { + return; + }; + + tracing::warn!( + "Semantic name '{}' of '{}' will be inherited by it's fields", + semantic_name, + var_input.var_layout.name().unwrap_or("<unnamed>") + ); + }); + + let mut seen_inputs = HashSet::<VertexInputSemName>::new(); + + Ok(Self { + inputs: inputs + .filter(|var_input| var_input.type_layout.kind() != TypeKind::Struct) + .map(|var_input| { + let name = var_input.var_layout.name().unwrap_or("<unnamed>"); + + let semantic_name = + var_input.var_layout.semantic_name().ok_or_else(|| { + VertexDescriptionError::VertexInputMissingSemanticName { + name: name.to_owned(), + } + })?; + + let semantic_name = + VertexInputSemName::from_semantic_name(semantic_name); + + if seen_inputs.contains(&semantic_name) { + return Err( + VertexDescriptionError::VertexInputHasOccupiedSemanticName { + name: name.to_owned(), + }, + ); + } + + let scalar_type = match ( + var_input.type_layout.kind(), + var_input.type_layout.scalar_type(), + ) { + (TypeKind::Scalar, Some(scalar_type)) => scalar_type, + (TypeKind::Vector, Some(scalar_type)) => scalar_type, + _ => { + return Err( + VertexDescriptionError::UnsupportedVertexInputType { + name: name.to_owned(), + }, + ); + } + }; + + seen_inputs.insert(semantic_name.clone()); + + Ok(VertexInputDescription { + semantic_name, + index: var_input.index, + type_kind: var_input.type_layout.kind(), + scalar_type, + }) + }) + .collect::<Result<Vec<_>, _>>()? + .into(), + }) + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub struct VertexInputDescription +{ + pub semantic_name: VertexInputSemName, + pub index: usize, + pub type_kind: TypeKind, + pub scalar_type: ScalarType, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum VertexInputSemName +{ + Position, + Normal, + UvFromTopLeft, + Color, + Other(Box<str>), +} + +impl VertexInputSemName +{ + fn from_semantic_name(semantic_name: &str) -> Self + { + match semantic_name { + STD_VERT_IN_SEM_NAME_POSITION => Self::Position, + STD_VERT_IN_SEM_NAME_NORMAL => Self::Normal, + STD_VERT_IN_SEM_NAME_UV_FROM_TOP_LEFT => Self::UvFromTopLeft, + STD_VERT_IN_SEM_NAME_COLOR => Self::Color, + _ => Self::Other(semantic_name.to_lowercase().into_boxed_str()), + } + } + + pub fn matches_vertex_label(&self, vertex_label: &VertexLabel) -> bool + { + match (self, vertex_label) { + (Self::Position, VertexLabel::Position) + | (Self::Normal, VertexLabel::Normal) + | (Self::UvFromTopLeft, VertexLabel::UvFromTopLeft) + | (Self::Color, VertexLabel::Color) => true, + (Self::Other(other), VertexLabel::Other(other_vertex_label)) => { + **other == *other_vertex_label + } + _ => false, + } + } +} + +impl Display for VertexInputSemName +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + match self { + Self::Position => formatter.write_str(STD_VERT_IN_SEM_NAME_POSITION), + Self::Normal => formatter.write_str(STD_VERT_IN_SEM_NAME_NORMAL), + Self::UvFromTopLeft => { + formatter.write_str(STD_VERT_IN_SEM_NAME_UV_FROM_TOP_LEFT) + } + Self::Color => formatter.write_str(STD_VERT_IN_SEM_NAME_COLOR), + Self::Other(other) => { + for character in other.chars() { + formatter.write_char(character.to_ascii_uppercase())?; + } + + Ok(()) + } + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum VertexDescriptionError +{ + #[error("Entrypoint is not in vertex stage")] + EntrypointNotInVertexStage, + + #[error("Type of vertex input '{name}' is not supported")] + UnsupportedVertexInputType + { + name: String + }, + + #[error("Vertex input '{name}' is missing a semantic name")] + VertexInputMissingSemanticName + { + name: String + }, + + #[error( + "Vertex input '{name}' has a semantic name already used by another vertex input" + )] + VertexInputHasOccupiedSemanticName + { + name: String + }, +} + +#[derive(Debug, thiserror::Error)] +pub enum ComposeProgramError +{ + #[error("Shader module does not have a '{entrypoint_name}' entry point")] + EntrypointNotFoundError + { + entrypoint_name: &'static str + }, + + #[error("Failed to create vertex description")] + VertexDescriptionCreationFailed(#[from] VertexDescriptionError), + + #[error(transparent)] + Other(Error), +} + +#[derive(Debug, thiserror::Error)] +#[error(transparent)] +pub struct Error(#[from] shader_slang::Error); + +struct VarInputDfsIter<'a> +{ + stack: Vec<(VariableLayout<'a>, TypeLayout<'a>, usize)>, +} + +impl<'a> VarInputDfsIter<'a> +{ + fn new(entry_point: &EntryPointReflection<'a>) -> Self + { + Self { + stack: entry_point + .parameters() + .map(|param| { + let Some(param_type_layout) = param.type_layout() else { + // I do not know in which cases type_layout can return None + unimplemented!(); + }; + + (param, param_type_layout, 0) + }) + .collect(), + } + } +} + +impl<'a> Iterator for VarInputDfsIter<'a> +{ + type Item = VarInput<'a>; + + fn next(&mut self) -> Option<Self::Item> + { + let (var_layout, type_layout, index) = loop { + let (var_layout, type_layout, acc_offset) = self.stack.pop()?; + + if let Some(offset) = var_layout.varying_input_offset() { + break (var_layout, type_layout, acc_offset + offset); + } + }; + + if type_layout.kind() == TypeKind::Struct { + self.stack.extend(type_layout.fields().map(|field| { + let Some(field_type_layout) = field.type_layout() else { + // I do not know in which cases type_layout can return + // None + unimplemented!(); + }; + + (field, field_type_layout, index) + })); + } + + Some(VarInput { var_layout, type_layout, index }) + } +} + +struct VarInput<'a> +{ + var_layout: VariableLayout<'a>, + type_layout: TypeLayout<'a>, + index: usize, +} + +fn import_slang_asset( + asset_submitter: &mut AssetSubmitter<'_>, + file_path: &Path, + settings: Option<&'_ Settings>, +) -> Result<(), ImportError> +{ + let file_name = file_path + .file_name() + .ok_or(ImportError::NoPathFileName)? + .to_str() + .ok_or(ImportError::PathFileNameNotUtf8)?; + + let file_path_canonicalized = file_path + .canonicalize() + .map_err(ImportError::CanonicalizePathFailed)?; + + asset_submitter.submit_store(ModuleSource { + name: file_name.to_owned().into(), + file_path: file_path_canonicalized.into(), + source: std::fs::read_to_string(file_path) + .map_err(ImportError::ReadFileFailed)? + .into(), + link_entrypoints: settings + .map(|settings| settings.link_entrypoints) + .unwrap_or_default(), + }); + + Ok(()) +} + +#[derive(Debug, thiserror::Error)] +enum ImportError +{ + #[error("Failed to read file")] + ReadFileFailed(#[source] std::io::Error), + + #[error("Asset path does not have a file name")] + NoPathFileName, + + #[error("Asset path file name is not valid UTF8")] + PathFileNameNotUtf8, + + #[error("Failed to canonicalize asset path")] + CanonicalizePathFailed(#[source] std::io::Error), +} + +declare_entity! { +pub IMPORT_SHADERS_PHASE: (Phase, pair!(ChildOf, { *HANDLE_ASSETS_PHASE })); +} + +pub(super) fn prepare(collector: &mut crate::ecs::extension::Collector<'_>) +{ + let Some(global_session) = SlangGlobalSession::new() else { + tracing::error!("Unable to create global shader-slang session"); + return; + }; + + let session_options = shader_slang::CompilerOptions::default() + .optimization(shader_slang::OptimizationLevel::None) + .matrix_layout_row(true) + .debug_information(SlangDebugInfoLevel::Maximal) + .no_mangle(true); + + let target_desc = shader_slang::TargetDesc::default() + .format(shader_slang::CompileTarget::Glsl) + // .format(shader_slang::CompileTarget::Spirv) + .profile(global_session.find_profile("glsl_330")); + // .profile(global_session.find_profile("spirv_1_5")); + + let targets = [target_desc]; + + let session_desc = shader_slang::SessionDesc::default() + .targets(&targets) + .search_paths(&[""]) + .options(&session_options); + + let Some(session) = global_session.create_session(&session_desc) else { + tracing::error!("Failed to create shader-slang session"); + return; + }; + + collector + .add_sole(Context { + _global_session: global_session, + session, + modules: HashMap::new(), + programs: HashMap::new(), + }) + .ok(); + + collector.spawn_declared_entity(&IMPORT_SHADERS_PHASE); + + collector.add_system(*INIT_PHASE, initialize); + collector.add_system(*IMPORT_SHADERS_PHASE, load_modules); +} + +fn initialize(mut assets: Single<Assets>) -> Result<(), crate::Error> +{ + let assets = assets.get_mut()?; + + assets.set_importer::<_, _>(["slang"], import_slang_asset); + + Ok(()) +} + +#[tracing::instrument(skip_all)] +fn load_modules( + mut context: Single<Context>, + assets: Single<Assets>, +) -> Result<(), crate::Error> +{ + let Ok(context) = context.get_mut() else { + unreachable!(); + }; + + let assets = assets.get()?; + + for AssetEvent::Stored(asset_id, asset_label) in assets.events().last_tick_events() { + let asset_handle = AssetHandle::<ModuleSource>::from_id(*asset_id); + + if !assets.is_loaded_and_has_type(&asset_handle) { + continue; + } + + let Some(module_source) = assets.get(&asset_handle) else { + unreachable!(); + }; + + tracing::debug!(asset_label=?asset_label, "Loading shader module"); + + let module = match load_module(&context.session, module_source) { + Ok(module) => module, + Err(err) => { + tracing::error!("Failed to load shader module: {err}"); + continue; + } + }; + + context.modules.insert(*asset_id, module.clone()); + + if !module_source.link_entrypoints.is_empty() { + assert!(context.programs.get(asset_id).is_none()); + + let shader_program = match context + .compose_into_program(module, module_source.link_entrypoints) + { + Ok(shader_program) => shader_program, + Err(err) => { + tracing::error!( + "Failed to compose shader into program: {:#}", + crate::Error::new(err) + ); + continue; + } + }; + + let linked_shader_program = match shader_program.into_linked() { + Ok(linked_shader_program) => linked_shader_program, + Err(err) => { + tracing::error!("Failed to link shader: {err}"); + continue; + } + }; + + context.programs.insert(*asset_id, linked_shader_program); + } + } + + Ok(()) +} + +fn load_module( + session: &SlangSession, + module_source: &ModuleSource, +) -> Result<Module, Error> +{ + let module = session.load_module_from_source_string( + &module_source.name, + &module_source.file_path.to_string_lossy(), + &module_source.source, + )?; + + Ok(Module { inner: module }) +} diff --git a/engine/src/rendering/shader/cursor.rs b/engine/src/rendering/shader/cursor.rs new file mode 100644 index 0000000..49f6b47 --- /dev/null +++ b/engine/src/rendering/shader/cursor.rs @@ -0,0 +1,372 @@ +use std::borrow::Cow; +use std::fmt::Display; +use std::hint::cold_path; + +use circular_buffer::FixedCircularBuffer; + +use crate::color::{Color, Rgb, Rgba}; +use crate::data_types::matrix::Matrix; +use crate::data_types::vector::Vec3; +use crate::rendering::object::Id as RenderingObjectId; +use crate::rendering::shader::{ + ResourceShape, + ScalarType, + TypeKind, + TypeLayout, + VariableLayout, +}; + +/// Shader cursor +#[derive(Clone)] +pub struct Cursor<'a> +{ + type_layout: TypeLayout<'a>, + binding_location: BindingLocation, + location_path: LocationPath, +} + +impl<'a> Cursor<'a> +{ + pub fn new(var_layout: VariableLayout<'a>) -> Self + { + let binding_location = BindingLocation { + binding_index: var_layout.binding_index(), + binding_size: 0, + byte_offset: var_layout.offset(), + }; + + Self { + type_layout: var_layout.type_layout().unwrap(), + binding_location, + location_path: LocationPath::default(), + } + } + + pub fn field(&self, name: impl Into<Cow<'static, str>>) -> Self + { + let name = name.into(); + + let Some(field_var_layout) = self.type_layout.get_field_by_name(name.as_ref()) + else { + panic!("Field '{name}' does not exist"); + }; + + let field_type_kind = field_var_layout.ty().unwrap().kind(); + + let (new_var_layout, binding_index_offset) = match field_type_kind { + TypeKind::ConstantBuffer => { + let elem_var_layout = field_var_layout + .type_layout() + .expect("Constant buffer field has no type layout") + .element_var_layout() + .expect( + "Constant buffer field type layout has no element var layout", + ); + + ( + elem_var_layout, + field_var_layout.binding_index() + elem_var_layout.binding_index(), + ) + } + TypeKind::Array + | TypeKind::Matrix + | TypeKind::Scalar + | TypeKind::Vector + | TypeKind::Struct + | TypeKind::Resource => (field_var_layout, field_var_layout.binding_index()), + type_kind => unimplemented!("Type kind {type_kind:?} is not yet supported"), + }; + + let mut location_path = self.location_path.clone(); + + location_path.push(Location::Field(name)); + + Self { + type_layout: new_var_layout.type_layout().unwrap(), + binding_location: BindingLocation { + binding_index: self.binding_location.binding_index + binding_index_offset, + binding_size: if field_type_kind == TypeKind::ConstantBuffer { + new_var_layout + .type_layout() + .unwrap() + .uniform_size() + .unwrap() + } else { + self.binding_location.binding_size + }, + byte_offset: self.binding_location.byte_offset + new_var_layout.offset(), + }, + location_path, + } + } + + pub fn element(mut self, index: usize) -> Self + { + let element_type_layout = self.type_layout.element_type_layout().unwrap(); + + self.binding_location.byte_offset += index * element_type_layout.stride(); + + self.type_layout = element_type_layout; + + self.location_path.push(Location::Element(index)); + + self + } + + pub fn binding(self, value: BindingValue) -> Result<Binding, BindingError> + { + value.validate_for_shader_type(self.type_layout, self.location_path)?; + + Ok(Binding { + location: self.binding_location, + value, + }) + } +} + +/// Shader cursor location. +#[derive(Debug, Clone, Default)] +pub struct LocationPath +{ + locations: FixedCircularBuffer<Location, 16>, + is_truncated: bool, +} + +impl LocationPath +{ + fn push(&mut self, location: Location) + { + if self.locations.push_back(location).is_some() { + self.is_truncated = true; + } + } +} + +impl Display for LocationPath +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + if self.is_truncated { + write!(formatter, "(...)")?; + } + + for location in &self.locations { + match location { + Location::Field(field) => { + write!(formatter, ".{field}")?; + } + Location::Element(element) => { + write!(formatter, "[{element}]")?; + } + } + } + + Ok(()) + } +} + +/// Shader cursor location. +#[derive(Debug, Clone)] +pub enum Location +{ + Field(Cow<'static, str>), + Element(usize), +} + +impl Display for Location +{ + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + match self { + Self::Field(field) => <Cow<'static, str> as Display>::fmt(field, formatter), + Self::Element(element) => <usize as Display>::fmt(element, formatter), + } + } +} + +#[derive(Debug, Clone)] +pub struct BindingLocation +{ + pub binding_index: u32, + pub binding_size: usize, + pub byte_offset: usize, +} + +#[derive(Debug, Clone)] +pub enum BindingValue +{ + Uint(u32), + Int(i32), + Float(f32), + FVec3(Vec3<f32>), + Color(Color<f32>), + FMat4x4(Matrix<f32, 4, 4>), + Texture(RenderingObjectId, BindingTextureKind), +} + +impl BindingValue +{ + fn validate_for_shader_type( + &self, + ty: TypeLayout<'_>, + location_path: LocationPath, + ) -> Result<(), BindingError> + { + let ty_kind = ty.kind(); + let scalar_ty = ty.scalar_type(); + + let element_scalar_ty = ty + .element_type_layout() + .and_then(|elem_ty| elem_ty.scalar_type()); + + let element_cnt = ty.element_cnt(); + + let is_valid = match self { + Self::Uint(_) => { + ty_kind == TypeKind::Scalar && scalar_ty == Some(ScalarType::Uint32) + } + Self::Int(_) => { + ty_kind == TypeKind::Scalar && scalar_ty == Some(ScalarType::Int32) + } + Self::Float(_) => { + ty_kind == TypeKind::Scalar && scalar_ty == Some(ScalarType::Float32) + } + Self::FVec3(_) => { + ty_kind == TypeKind::Vector + && element_scalar_ty == Some(ScalarType::Float32) + && element_cnt == Some(3) + } + Self::Color(Color::Rgb(_)) => { + ty_kind == TypeKind::Vector + && element_scalar_ty == Some(ScalarType::Float32) + && element_cnt == Some(3) + } + Self::Color(Color::Rgba(_)) => { + ty_kind == TypeKind::Vector + && element_scalar_ty == Some(ScalarType::Float32) + && element_cnt == Some(4) + } + Self::FMat4x4(_) => { + ty_kind == TypeKind::Matrix + && element_scalar_ty == Some(ScalarType::Float32) + && ty.row_cnt() == Some(4) + && ty.column_cnt() == Some(4) + } + Self::Texture(_, BindingTextureKind::Texture2D) => { + ty_kind == TypeKind::Resource + && ty.resource_shape().is_some_and(|res_shape| { + (res_shape & ResourceShape::BASE) + .contains(ResourceShape::TEXTURE_2D) + }) + } + Self::Texture(_, BindingTextureKind::Cube) => { + ty_kind == TypeKind::Resource + && ty.resource_shape().is_some_and(|res_shape| { + (res_shape & ResourceShape::BASE) + .contains(ResourceShape::TEXTURE_CUBE) + }) + } + }; + + if !is_valid { + cold_path(); + return Err(BindingError::IncorrectValueType { + value: self.clone(), + location_path, + }); + } + + Ok(()) + } +} + +impl From<u32> for BindingValue +{ + fn from(value: u32) -> Self + { + BindingValue::Uint(value) + } +} + +impl From<i32> for BindingValue +{ + fn from(value: i32) -> Self + { + BindingValue::Int(value) + } +} + +impl From<f32> for BindingValue +{ + fn from(value: f32) -> Self + { + BindingValue::Float(value) + } +} + +impl From<Vec3<f32>> for BindingValue +{ + fn from(vec: Vec3<f32>) -> Self + { + BindingValue::FVec3(vec) + } +} + +impl From<Color<f32>> for BindingValue +{ + fn from(color: Color<f32>) -> Self + { + BindingValue::Color(color) + } +} +impl From<Rgb<f32>> for BindingValue +{ + fn from(color: Rgb<f32>) -> Self + { + BindingValue::Color(color.into()) + } +} + +impl From<Rgba<f32>> for BindingValue +{ + fn from(color: Rgba<f32>) -> Self + { + BindingValue::Color(color.into()) + } +} + +impl From<Matrix<f32, 4, 4>> for BindingValue +{ + fn from(matrix: Matrix<f32, 4, 4>) -> Self + { + BindingValue::FMat4x4(matrix) + } +} + +#[derive(Debug, Clone)] +#[non_exhaustive] +pub struct Binding +{ + pub location: BindingLocation, + pub value: BindingValue, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum BindingTextureKind +{ + Texture2D, + Cube, +} + +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum BindingError +{ + #[error("Value {value:?} has incorrect type for value at {location_path} in shader")] + IncorrectValueType + { + value: BindingValue, + location_path: LocationPath, + }, +} |
