summaryrefslogtreecommitdiff
path: root/engine/src/rendering
diff options
context:
space:
mode:
Diffstat (limited to 'engine/src/rendering')
-rw-r--r--engine/src/rendering/backend.rs9
-rw-r--r--engine/src/rendering/backend/opengl.rs2060
-rw-r--r--engine/src/rendering/backend/opengl/glutin_compat.rs264
-rw-r--r--engine/src/rendering/backend/opengl/graphics_mesh.rs259
-rw-r--r--engine/src/rendering/blending.rs89
-rw-r--r--engine/src/rendering/main_render_pass.rs1006
-rw-r--r--engine/src/rendering/object.rs137
-rw-r--r--engine/src/rendering/shader.rs1404
-rw-r--r--engine/src/rendering/shader/cursor.rs372
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,
+ },
+}