kitgit

tirbofish/dropbear · commit

b2ef470621be702879257bc5740addc68a72b2f8

feature: mipmaps (fixes #77) Unverified

Thribhu K <4tkbytes@pm.me> · 2026-01-19 13:57

view full diff

diff --git a/crates/dropbear-engine/src/asset.rs b/crates/dropbear-engine/src/asset.rs
index 4cdd052..49c4aa3 100644
--- a/crates/dropbear-engine/src/asset.rs
+++ b/crates/dropbear-engine/src/asset.rs
@@ -140,7 +140,16 @@ impl AssetRegistry {
             return Arc::clone(existing.value());
         }
 
-        let texture = Texture::from_bytes_verbose(&graphics.device, &graphics.queue, &rgba, Some((1, 1)), None, None, None);
+        let label = format!("Solid texture [{}, {}, {}, {}]", rgba[0], rgba[1], rgba[2], rgba[3]);
+
+        let texture = Texture::from_bytes_verbose_mipmapped(
+            graphics,
+            &rgba,
+            Some((1, 1)),
+            None,
+            None,
+            Some(label.as_str())
+        );
         let texture = Arc::new(texture);
 
         match self.solid_textures.entry(key) {
diff --git a/crates/dropbear-engine/src/graphics.rs b/crates/dropbear-engine/src/graphics.rs
index b93c290..6a085db 100644
--- a/crates/dropbear-engine/src/graphics.rs
+++ b/crates/dropbear-engine/src/graphics.rs
@@ -1,3 +1,4 @@
+use std::ops::{Deref, DerefMut};
 use crate::{BindGroupLayouts, texture};
 use crate::{
     State,
@@ -11,12 +12,9 @@ use std::sync::Arc;
 use wgpu::*;
 use winit::window::Window;
 
-pub const NO_TEXTURE: &[u8] = include_bytes!("../../../resources/textures/no-texture.png");
+use crate::mipmap::MipMapper;
 
-pub struct FrameGraphicsContext<'a> {
-    pub view: TextureView,
-    pub encoder: &'a mut CommandEncoder,
-}
+pub const NO_TEXTURE: &[u8] = include_bytes!("../../../resources/textures/no-texture.png");
 
 pub struct SharedGraphicsContext {
     pub device: Arc<Device>,
@@ -32,6 +30,7 @@ pub struct SharedGraphicsContext {
     pub texture_id: Arc<TextureId>,
     pub future_queue: Arc<FutureQueue>,
     pub supports_storage: bool,
+    pub mipmapper: Arc<MipMapper>,
 }
 
 impl SharedGraphicsContext {
@@ -73,6 +72,7 @@ impl SharedGraphicsContext {
             surface: state.surface.clone(),
             surface_format: state.surface_format,
             supports_storage: state.supports_storage,
+            mipmapper: state.mipmapper.clone(),
         }
     }
 }
@@ -187,3 +187,49 @@ impl InstanceRaw {
         }
     }
 }
+
+
+/// A wrapper to the [wgpu::CommandEncoder]
+pub struct CommandEncoder {
+    inner: wgpu::CommandEncoder,
+}
+
+impl Deref for CommandEncoder {
+    type Target = wgpu::CommandEncoder;
+
+    fn deref(&self) -> &Self::Target {
+        &self.inner
+    }
+}
+
+impl DerefMut for CommandEncoder {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.inner
+    }
+}
+
+impl CommandEncoder {
+    /// Creates a new instance of a command encoder. 
+    pub fn new(graphics: Arc<SharedGraphicsContext>, label: Option<&str>) -> Self {
+        Self {
+            inner: graphics.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label }),
+        }
+    }
+    
+    /// Submits the command encoder for execution. 
+    /// 
+    /// Panics if an unwinding error is caught, or just returns the error as normal. 
+    pub fn submit(self, graphics: Arc<SharedGraphicsContext>) -> anyhow::Result<()> {
+        let command_buffer = self.inner.finish();
+
+        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+            graphics.queue.submit(std::iter::once(command_buffer));
+        })) {
+            Ok(_) => {Ok(())}
+            Err(_) => {
+                log::error!("Failed to submit command buffer, device may be lost");
+                return Err(anyhow::anyhow!("Command buffer submission failed"));
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index efad8e7..2d3af5b 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -17,6 +17,7 @@ pub mod shader;
 pub mod utils;
 pub mod texture;
 pub mod pipelines;
+pub mod mipmap;
 
 pub static WGPU_BACKEND: OnceLock<String> = OnceLock::new();
 pub const PHYSICS_STEP_RATE: u32 = 120;
@@ -50,10 +51,11 @@ use winit::{
 };
 
 use crate::camera::Camera;
-use crate::graphics::{FrameGraphicsContext, SharedGraphicsContext};
+use crate::graphics::{CommandEncoder, SharedGraphicsContext};
 use crate::lighting::{Light};
 use crate::texture::Texture;
 use crate::{egui_renderer::EguiRenderer};
+use crate::mipmap::MipMapper;
 
 pub use dropbear_future_queue as future;
 pub use gilrs;
@@ -91,6 +93,7 @@ pub struct State {
     pub viewport_texture: Texture,
     pub texture_id: Arc<TextureId>,
     pub future_queue: Arc<FutureQueue>,
+    pub mipmapper: Arc<MipMapper>,
 
     physics_accumulator: Duration,
 
@@ -233,6 +236,8 @@ Hardware:
         let viewport_texture =
             Texture::viewport(&config, &device, Some("viewport texture"));
 
+        let mipmapper = Arc::new(MipMapper::new(&device));
+
         let mut egui_renderer = Arc::new(Mutex::new(EguiRenderer::new(
             &device,
             config.format,
@@ -335,6 +340,7 @@ Hardware:
             viewport_texture,
             texture_id: Arc::new(texture_id),
             future_queue,
+            mipmapper,
             instance,
             physics_accumulator: Duration::ZERO,
             scene_manager: scene::Manager::new(),
@@ -427,17 +433,32 @@ Hardware:
             .texture
             .create_view(&wgpu::TextureViewDescriptor::default());
 
-        let mut encoder = self
-            .device
-            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
-                label: Some("Render Encoder"),
-            });
-
-        let frame_ctx = FrameGraphicsContext {
-            view: view.clone(),
-            encoder: &mut encoder,
-        };
+        { // ensures clearing of the encoder is done correctly. 
+            let mut encoder = CommandEncoder::new(graphics.clone(), Some("surface clear render encoder"));
+
+            {
+                let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                    label: Some("surface clear pass"),
+                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                        view: &view,
+                        depth_slice: None,
+                        resolve_target: None,
+                        ops: wgpu::Operations {
+                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
+                            store: wgpu::StoreOp::Store,
+                        },
+                    })],
+                    depth_stencil_attachment: None,
+                    occlusion_query_set: None,
+                    timestamp_writes: None,
+                });
+            }
 
+            if let Err(e) = encoder.submit(graphics.clone()) {
+                log_once::error_once!("{}", e);
+            }
+        }
+        
         self.egui_renderer.lock().begin_frame(&self.window);
 
         let mut scene_manager = std::mem::replace(&mut self.scene_manager, scene::Manager::new());
@@ -459,7 +480,7 @@ Hardware:
             }
 
             let commands = scene_manager.update(previous_dt, graphics.clone(), event_loop);
-            scene_manager.render(graphics.clone(), frame_ctx);
+            scene_manager.render(graphics.clone());
             commands
         };
 
@@ -467,6 +488,12 @@ Hardware:
 
         self.scene_manager = scene_manager;
 
+        let mut encoder = self
+            .device
+            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
+                label: Some("egui render encoder"),
+            });
+
         self.egui_renderer.lock().end_frame_and_draw(
             &self.device,
             &self.queue,
diff --git a/crates/dropbear-engine/src/mipmap.rs b/crates/dropbear-engine/src/mipmap.rs
new file mode 100644
index 0000000..d397eca
--- /dev/null
+++ b/crates/dropbear-engine/src/mipmap.rs
@@ -0,0 +1,390 @@
+use crate::texture::Texture;
+
+pub struct MipMapper {
+    blit_mipmap: wgpu::RenderPipeline,
+    blit_sampler: wgpu::Sampler,
+    compute_pipeline: wgpu::ComputePipeline,
+    storage_texture_layout: wgpu::BindGroupLayout,
+    pub enabled: bool,
+}
+
+impl MipMapper {
+    pub fn new(device: &wgpu::Device) -> Self {
+        let blit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+            label: Some("mipmap blit shader"),
+            source: wgpu::ShaderSource::Wgsl(include_str!("pipelines/shaders/blit.wgsl").into()),
+        });
+
+        // Keep this SRGB so we can render directly into the SRGB textures we create for materials.
+        let blit_format = Texture::TEXTURE_FORMAT;
+        let blit_mipmap = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+            label: Some("mipmap blit render pipeline"),
+            layout: None,
+            vertex: wgpu::VertexState {
+                module: &blit_shader,
+                entry_point: None,
+                buffers: &[],
+                compilation_options: Default::default(),
+            },
+            fragment: Some(wgpu::FragmentState {
+                module: &blit_shader,
+                entry_point: None,
+                compilation_options: Default::default(),
+                targets: &[
+                    Some(wgpu::ColorTargetState {
+                        format: blit_format,
+                        blend: None,
+                        write_mask: wgpu::ColorWrites::ALL,
+                    })
+                ],
+            }),
+            primitive: wgpu::PrimitiveState {
+                topology: wgpu::PrimitiveTopology::TriangleList,
+                front_face: wgpu::FrontFace::Cw, // change if issues
+                cull_mode: Some(wgpu::Face::Back),
+                strip_index_format: None,
+                polygon_mode: wgpu::PolygonMode::Fill,
+                ..Default::default()
+            },
+            depth_stencil: None,
+            multisample: wgpu::MultisampleState {
+                count: 1,
+                mask: !0,
+                alpha_to_coverage_enabled: false,
+            },
+            cache: None,
+            multiview: None,
+        });
+
+        let storage_texture_layout =
+            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+                label: Some("Mipmapper::texture_layout"),
+                entries: &[
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 0,
+                        visibility: wgpu::ShaderStages::COMPUTE,
+                        ty: wgpu::BindingType::StorageTexture {
+                            access: wgpu::StorageTextureAccess::ReadOnly,
+                            format: wgpu::TextureFormat::Rgba8Unorm,
+                            view_dimension: wgpu::TextureViewDimension::D2,
+                        },
+                        count: None,
+                    },
+                    wgpu::BindGroupLayoutEntry {
+                        binding: 1,
+                        visibility: wgpu::ShaderStages::COMPUTE,
+                        ty: wgpu::BindingType::StorageTexture {
+                            access: wgpu::StorageTextureAccess::WriteOnly,
+                            format: wgpu::TextureFormat::Rgba8Unorm,
+                            view_dimension: wgpu::TextureViewDimension::D2,
+                        },
+                        count: None,
+                    },
+                ],
+            });
+
+        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: None,
+            bind_group_layouts: &[&storage_texture_layout],
+            push_constant_ranges: &[],
+        });
+        let compute_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+            label: Some("mipmap compute shader"),
+            source: wgpu::ShaderSource::Wgsl(include_str!("pipelines/shaders/mipmap.wgsl").into()),
+        });
+        let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
+            label: Some("Mipmapper"),
+            layout: Some(&pipeline_layout),
+            module: &compute_shader,
+            entry_point: Some("compute_mipmap"),
+            compilation_options: Default::default(),
+            cache: None,
+        });
+
+        let blit_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
+            min_filter: wgpu::FilterMode::Linear,
+            mag_filter: wgpu::FilterMode::Linear,
+            ..Default::default()
+        });
+
+        Self {
+            storage_texture_layout,
+            compute_pipeline,
+            blit_mipmap,
+            blit_sampler,
+            enabled: true,
+        }
+    }
+
+    pub fn blit_mipmaps(
+        &self,
+        device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        texture: &Texture,
+    ) -> anyhow::Result<()> {
+        let texture = &texture.texture;
+
+        match texture.format() {
+            wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => {},
+            _ => anyhow::bail!("Unsupported format {:?}", texture.format()),
+        }
+
+        if texture.mip_level_count() == 1 {
+            return Ok(());
+        }
+
+        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
+            label: Some("blit mipmap encoder"),
+        });
+
+        // We need to render to this texture, so if the supplied texture
+        // isn't setup for rendering, we need to create a temporary one.
+        let (mut src_view, maybe_temp) = if texture.usage().contains(wgpu::TextureUsages::RENDER_ATTACHMENT) {
+            (
+                texture.create_view(&wgpu::TextureViewDescriptor {
+                    base_mip_level: 0,
+                    mip_level_count: Some(1),
+                    ..Default::default()
+                }),
+                None,
+            )
+        } else {
+            // Create a temporary texture that can be rendered to since the
+            // supplied texture can't be rendered to. It will be basically
+            // identical to the original apart from the usage field and removing
+            // sRGB from the format if it's present.
+            let temp = device.create_texture(&wgpu::TextureDescriptor {
+                label: Some("Mipmapper::blit_mipmaps::temp"),
+                size: texture.size(),
+                mip_level_count: texture.mip_level_count(),
+                sample_count: texture.sample_count(),
+                dimension: texture.dimension(),
+                format: texture.format(),
+                usage: wgpu::TextureUsages::RENDER_ATTACHMENT
+                    | wgpu::TextureUsages::TEXTURE_BINDING
+                    | wgpu::TextureUsages::COPY_DST
+                    | wgpu::TextureUsages::COPY_SRC,
+                view_formats: &[],
+            });
+
+            encoder.copy_texture_to_texture(
+                texture.as_image_copy(),
+                temp.as_image_copy(),
+                temp.size(),
+            );
+
+            (
+                temp.create_view(&wgpu::TextureViewDescriptor {
+                    mip_level_count: Some(1),
+                    ..Default::default()
+                }),
+                Some(temp),
+            )
+        };
+
+        for mip in 1..texture.mip_level_count() {
+            let dst_view = src_view
+                .texture()
+                .create_view(&wgpu::TextureViewDescriptor {
+                    // What mip we want to render to
+                    base_mip_level: mip,
+                    // Like src_view we need to ignore other mips
+                    mip_level_count: Some(1),
+                    ..Default::default()
+                });
+
+            let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+                label: None,
+                layout: &self.blit_mipmap.get_bind_group_layout(0),
+                entries: &[
+                    wgpu::BindGroupEntry {
+                        binding: 0,
+                        resource: wgpu::BindingResource::TextureView(&src_view),
+                    },
+                    wgpu::BindGroupEntry {
+                        binding: 1,
+                        resource: wgpu::BindingResource::Sampler(&self.blit_sampler),
+                    },
+                ],
+            });
+
+            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: None,
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: &dst_view,
+                    depth_slice: None,
+                    resolve_target: None,
+                    ops: wgpu::Operations {
+                        load: wgpu::LoadOp::Load,
+                        store: wgpu::StoreOp::Store,
+                    },
+                })],
+                depth_stencil_attachment: None,
+                timestamp_writes: None,
+                occlusion_query_set: None,
+            });
+            pass.set_pipeline(&self.blit_mipmap);
+            pass.set_bind_group(0, &texture_bind_group, &[]);
+            pass.draw(0..3, 0..1);
+
+            // Make sure that we use the mip we just generated for the
+            // next iteration.
+            src_view = dst_view;
+        }
+
+        // If we created a temporary texture, now we need to copy it back
+        // into the original.
+        if let Some(temp) = maybe_temp {
+            let mut size = temp.size();
+            for mip_level in 0..temp.mip_level_count() {
+                encoder.copy_texture_to_texture(
+                    wgpu::TexelCopyTextureInfo {
+                        mip_level,
+                        ..temp.as_image_copy()
+                    },
+                    wgpu::TexelCopyTextureInfo {
+                        mip_level,
+                        ..texture.as_image_copy()
+                    },
+                    size,
+                );
+
+                // Each mipmap is half the size of the original,
+                // so we need to half the copy size as well.
+                size.width /= 2;
+                size.height /= 2;
+            }
+        }
+
+        // Submit directly (this is a standalone utility).
+        let command_buffer = encoder.finish();
+        queue.submit(std::iter::once(command_buffer));
+
+        Ok(())
+    }
+
+    pub fn compute_mipmaps(
+        &self,
+        device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        texture: &Texture,
+    ) -> anyhow::Result<()> {
+        let texture = &texture.texture;
+
+        match texture.format() {
+            wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => {}
+            _ => anyhow::bail!("Unsupported format {:?}", texture.format()),
+        }
+
+        if texture.mip_level_count() == 1 {
+            return Ok(());
+        }
+
+        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
+            label: Some("compute mipmap encoder"),
+        });
+
+        // Create temp texture if supplied texture isn't setup for use
+        // as a storage texture
+        let (mut src_view, maybe_temp) = if texture
+            .usage()
+            .contains(wgpu::TextureUsages::STORAGE_BINDING)
+        {
+            (
+                texture.create_view(&wgpu::TextureViewDescriptor {
+                    mip_level_count: Some(1),
+                    ..Default::default()
+                }),
+                None,
+            )
+        } else {
+            let temp = device.create_texture(&wgpu::TextureDescriptor {
+                label: Some("Mipmapper::compute_mipmaps::temp"),
+                size: texture.size(),
+                mip_level_count: texture.mip_level_count(),
+                sample_count: texture.sample_count(),
+                dimension: texture.dimension(),
+                format: texture.format().remove_srgb_suffix(),
+                usage: wgpu::TextureUsages::STORAGE_BINDING
+                    | wgpu::TextureUsages::COPY_DST
+                    | wgpu::TextureUsages::COPY_SRC,
+                view_formats: &[],
+            });
+
+            encoder.copy_texture_to_texture(
+                texture.as_image_copy(),
+                temp.as_image_copy(),
+                temp.size(),
+            );
+
+            (
+                temp.create_view(&wgpu::TextureViewDescriptor {
+                    mip_level_count: Some(1),
+                    ..Default::default()
+                }),
+                Some(temp),
+            )
+        };
+
+        let dispatch_x = texture.width().div_ceil(16);
+        let dispatch_y = texture.height().div_ceil(16);
+
+        {
+            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor::default());
+            pass.set_pipeline(&self.compute_pipeline);
+            for mip in 1..texture.mip_level_count() {
+                let dst_view = src_view
+                    .texture()
+                    .create_view(&wgpu::TextureViewDescriptor {
+                        base_mip_level: mip,
+                        mip_level_count: Some(1),
+                        ..Default::default()
+                    });
+                let texture_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+                    label: None,
+                    layout: &self.storage_texture_layout,
+                    entries: &[
+                        wgpu::BindGroupEntry {
+                            binding: 0,
+                            resource: wgpu::BindingResource::TextureView(&src_view),
+                        },
+                        wgpu::BindGroupEntry {
+                            binding: 1,
+                            resource: wgpu::BindingResource::TextureView(&dst_view),
+                        },
+                    ],
+                });
+                pass.set_bind_group(0, &texture_bind_group, &[]);
+                pass.dispatch_workgroups(dispatch_x, dispatch_y, 1);
+
+                src_view = dst_view;
+            }
+        }
+
+        if let Some(temp) = maybe_temp {
+            let mut size = temp.size();
+            for mip_level in 0..temp.mip_level_count() {
+                encoder.copy_texture_to_texture(
+                    wgpu::TexelCopyTextureInfo {
+                        mip_level,
+                        ..temp.as_image_copy()
+                    },
+                    wgpu::TexelCopyTextureInfo {
+                        mip_level,
+                        ..texture.as_image_copy()
+                    },
+                    size,
+                );
+
+                // Each mipmap is half the size of the original
+                size.width /= 2;
+                size.height /= 2;
+            }
+        }
+
+        let command_buffer = encoder.finish();
+        queue.submit(std::iter::once(command_buffer));
+
+        Ok(())
+    }
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/model.rs b/crates/dropbear-engine/src/model.rs
index 56d03f5..93d69f7 100644
--- a/crates/dropbear-engine/src/model.rs
+++ b/crates/dropbear-engine/src/model.rs
@@ -250,28 +250,32 @@ impl Material {
         normal: &Texture,
         name: &str,
     ) -> BindGroup {
-        graphics.device.create_bind_group(&wgpu::BindGroupDescriptor {
-            layout: &graphics.layouts.texture_bind_layout,
-            entries: &[
-                wgpu::BindGroupEntry {
-                    binding: 0,
-                    resource: wgpu::BindingResource::TextureView(&diffuse.view),
-                },
-                wgpu::BindGroupEntry {
-                    binding: 1,
-                    resource: wgpu::BindingResource::Sampler(&diffuse.sampler),
-                },
-                wgpu::BindGroupEntry {
-                    binding: 2,
-                    resource: wgpu::BindingResource::TextureView(&normal.view),
-                },
-                wgpu::BindGroupEntry {
-                    binding: 3,
-                    resource: wgpu::BindingResource::Sampler(&normal.sampler),
-                },
-            ],
-            label: Some(name),
-        })
+        graphics.
+                device
+                .create_bind_group(&wgpu::BindGroupDescriptor {
+                    label: Some(format!("{} mipmapped compute bind group", name).as_str()),
+                    layout: &graphics.layouts.texture_bind_layout,
+                    entries: &[
+                        wgpu::BindGroupEntry {
+                            binding: 0,
+                            resource: wgpu::BindingResource::TextureView(
+                                &diffuse.view,
+                            ),
+                        },
+                        wgpu::BindGroupEntry {
+                            binding: 1,
+                            resource: wgpu::BindingResource::Sampler(&diffuse.sampler),
+                        },
+                        wgpu::BindGroupEntry {
+                            binding: 2,
+                            resource: wgpu::BindingResource::TextureView(&normal.view),
+                        },
+                        wgpu::BindGroupEntry {
+                            binding: 3,
+                            resource: wgpu::BindingResource::Sampler(&normal.sampler),
+                        },
+                    ],
+                })
     }
 
     pub fn set_tint(&mut self, graphics: &SharedGraphicsContext, tint: [f32; 4]) {
@@ -616,13 +620,27 @@ impl Model {
             let start = Instant::now();
 
             let diffuse_texture = if let Some((rgba_data, dimensions)) = processed_diffuse {
-                Texture::from_bytes_verbose(&graphics.device, &graphics.queue, &rgba_data, Some(dimensions), None, None, None)
+                Texture::from_bytes_verbose_mipmapped(
+                    graphics.clone(),
+                    &rgba_data,
+                    Some(dimensions),
+                    None,
+                    None,
+                    Some(material_name.as_str())
+                )
             } else {
                 (*grey_texture).clone()
             };
 
             let normal_texture = if let Some((rgba_data, dimensions)) = processed_normal {
-                Texture::from_bytes_verbose(&graphics.device, &graphics.queue, &rgba_data, Some(dimensions), None, None, None)
+                Texture::from_bytes_verbose_mipmapped(
+                    graphics.clone(),
+                    &rgba_data,
+                    Some(dimensions),
+                    None,
+                    None,
+                    Some(material_name.as_str())
+                )
             } else {
                 (*flat_normal_texture).clone()
             };
diff --git a/crates/dropbear-engine/src/pipelines/shaders/blit.wgsl b/crates/dropbear-engine/src/pipelines/shaders/blit.wgsl
new file mode 100644
index 0000000..aa10b01
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/shaders/blit.wgsl
@@ -0,0 +1,28 @@
+struct VertexOutput {
+    @builtin(position) clip_position: vec4<f32>,
+    @location(0) uv: vec2<f32>,
+}
+
+@group(0) @binding(0)
+var tex: texture_2d<f32>;
+@group(0) @binding(1)
+var tex_sampler: sampler;
+
+@vertex
+fn vs_main(
+    @builtin(vertex_index) in_vertex_index: u32,
+) -> VertexOutput {
+    var out: VertexOutput;
+    // Create fullscreen triangle
+    let x = f32((in_vertex_index << 1u) & 2u);
+    let y = f32(in_vertex_index & 2u);
+    out.clip_position = vec4<f32>(x * 2.0 - 1.0, y * 2.0 - 1.0, 0.0, 1.0);
+    out.uv = vec2<f32>(x, 1.0 - y);
+    return out;
+}
+
+@fragment
+fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
+    let s = textureSample(tex, tex_sampler, in.uv);
+    return s;
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/pipelines/shaders/mipmap.wgsl b/crates/dropbear-engine/src/pipelines/shaders/mipmap.wgsl
new file mode 100644
index 0000000..cfc908b
--- /dev/null
+++ b/crates/dropbear-engine/src/pipelines/shaders/mipmap.wgsl
@@ -0,0 +1,31 @@
+@group(0)
+@binding(0)
+var src: texture_storage_2d<rgba8unorm, read>;
+@group(0)
+@binding(1)
+var dst: texture_storage_2d<rgba8unorm, write>;
+
+@compute
+@workgroup_size(16, 16, 1)
+fn compute_mipmap(
+    @builtin(global_invocation_id) gid: vec3<u32>,
+) {
+    let dstPos = gid.xy;
+    let srcPos = gid.xy * 2;
+
+    let dim = textureDimensions(src);
+
+    if (dstPos.x >= dim.x || dstPos.y >= dim.y) {
+        return;
+    }
+
+    let t00 = textureLoad(src, srcPos);
+    let t01 = textureLoad(src, srcPos + vec2(0, 1));
+    let t10 = textureLoad(src, srcPos + vec2(1, 0));
+    let t11 = textureLoad(src, srcPos + vec2(1, 1));
+
+    // A simple linear average of 4 adjacent pixels
+    let t = (t00 + t01 + t10 + t11) * 0.25;
+
+    textureStore(dst, dstPos, t);
+}
diff --git a/crates/dropbear-engine/src/scene.rs b/crates/dropbear-engine/src/scene.rs
index a1a1082..5ceca77 100644
--- a/crates/dropbear-engine/src/scene.rs
+++ b/crates/dropbear-engine/src/scene.rs
@@ -5,7 +5,7 @@
 use winit::event_loop::ActiveEventLoop;
 use winit::window::WindowId;
 
-use crate::{WindowData, graphics::{FrameGraphicsContext, SharedGraphicsContext}, input};
+use crate::{WindowData, graphics::{SharedGraphicsContext}, input};
 use parking_lot::RwLock;
 use std::{collections::HashMap, rc::Rc, sync::Arc};
 
@@ -13,7 +13,7 @@ pub trait Scene {
     fn load(&mut self, graphics: Arc<SharedGraphicsContext>);
     fn physics_update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>);
     fn update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>);
-    fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, frame_ctx: FrameGraphicsContext<'a>);
+    fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>);
     fn exit(&mut self, event_loop: &ActiveEventLoop);
     /// By far a mess of a trait however it works.
     ///
@@ -160,11 +160,11 @@ impl Manager {
         }
     }
 
-    pub fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, frame_ctx: FrameGraphicsContext<'a>) {
+    pub fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) {
         if let Some(scene_name) = &self.current_scene
             && let Some(scene) = self.scenes.get_mut(scene_name)
         {
-            scene.write().render(graphics.clone(), frame_ctx)
+            scene.write().render(graphics.clone())
         }
     }
 
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index 0bb37b7..5cae83c 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -4,9 +4,9 @@ use image::GenericImageView;
 use serde::{Deserialize, Serialize};
 
 use crate::graphics::SharedGraphicsContext;
+use crate::utils::ToPotentialString;
 
-
-/// As defined in `shaders.wgsl` as 
+/// As defined in `shaders.wgsl` as
 /// ```
 /// @group(0) @binding(0)
 /// var t_diffuse: texture_2d<f32>;
@@ -63,6 +63,7 @@ pub const TEXTURE_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> =
 #[derive(Clone)]
 /// Describes a texture, like an image of some sort. Can be a normal texture on a model or a viewport or depth texture.
 pub struct Texture {
+    pub label: Option<String>,
     pub texture: wgpu::Texture,
     pub sampler: wgpu::Sampler,
     pub size: wgpu::Extent3d,
@@ -116,7 +117,8 @@ impl Texture {
             texture,
             sampler,
             size,
-            view
+            view,
+            label: label.to_potential_string(),
         }
     }
 
@@ -150,6 +152,7 @@ impl Texture {
         let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
 
         Self {
+            label: label.to_potential_string(),
             texture,
             sampler,
             size,
@@ -161,37 +164,61 @@ impl Texture {
     pub async fn from_file(
         graphics: Arc<SharedGraphicsContext>,
         path: &PathBuf,
+        label: Option<&str>,
     ) -> anyhow::Result<Self> {
         let data = fs::read(path)?;
-        Ok(Self::from_bytes(graphics.clone(), &data))
+        Ok(Self::from_bytes(graphics.clone(), &data, label))
     }
 
     /// Loads the texture from bytes.
     /// 
     /// If you want more customisability in the texture being generated, you can use [Self::from_bytes_verbose]
-    pub fn from_bytes(graphics: Arc<SharedGraphicsContext>, bytes: &[u8]) -> Self {
-        Self::from_bytes_verbose(
-            &graphics.device, 
-            &graphics.queue, 
-            bytes, 
-            None, 
-            None, 
+    pub fn from_bytes(graphics: Arc<SharedGraphicsContext>, bytes: &[u8], label: Option<&str>) -> Self {
+        Self::from_bytes_verbose_mipmapped(graphics, bytes, None, None, None, label)
+    }
+
+    /// Loads the texture from bytes and generates mipmaps on the GPU.
+    ///
+    /// This is the recommended constructor for any sampled texture used for rendering.
+    pub fn from_bytes_verbose_mipmapped(
+        graphics: Arc<SharedGraphicsContext>,
+        bytes: &[u8],
+        dimensions: Option<(u32, u32)>,
+        view_descriptor: Option<wgpu::TextureViewDescriptor>,
+        sampler: Option<wgpu::SamplerDescriptor>,
+        label: Option<&str>,
+    ) -> Self {
+        let texture = Self::from_bytes_verbose(
+            graphics.clone(),
+            bytes,
+            dimensions,
             None,
-            None
-        )
+            view_descriptor,
+            sampler,
+            label,
+        );
+
+        if let Err(err) = graphics
+            .mipmapper
+            .compute_mipmaps(&graphics.device, &graphics.queue, &texture)
+        {
+            log_once::warn_once!("Failed to generate mipmaps: {}", err);
+        }
+
+        texture
     }
 
     /// Loads the texture from bytes, with options for more arguments. 
     /// 
     /// Requires more arguments. For a simpler usage, you should use [Self::from_bytes]
     pub fn from_bytes_verbose(
-        device: &wgpu::Device,
-        queue: &wgpu::Queue,
+        graphics: Arc<SharedGraphicsContext>,
         bytes: &[u8],
         dimensions: Option<(u32, u32)>,
-        texture_descriptor: Option<wgpu::TextureDescriptor>,
+        _texture_descriptor: Option<wgpu::TextureDescriptor>,
         view_descriptor: Option<wgpu::TextureViewDescriptor>,
         sampler: Option<wgpu::SamplerDescriptor>,
+        label: Option<&str>,
     ) -> Self {
         let (diffuse_rgba, dimensions) = match image::load_from_memory(bytes) {
             Ok(image) => {
@@ -208,7 +235,8 @@ impl Texture {
                         (bytes.to_vec(), dims)
                     } else {
                         log::error!(
-                            "Texture decode failed ({:?}); expected {} bytes for raw RGBA ({}x{}), got {}. Falling back.",
+                            "Texture [{:?}] decode failed ({:?}); expected {} bytes for raw RGBA ({}x{}), got {}. Falling back.",
+                            label,
                             err,
                             expected_len,
                             dims.0,
@@ -219,40 +247,44 @@ impl Texture {
                     }
                 } else {
                     log::error!(
-                        "Texture decode failed ({:?}) and no dimensions were provided; falling back to 1x1 magenta.",
+                        "Texture [{:?}] decode failed ({:?}) and no dimensions were provided; falling back to 1x1 magenta.",
+                        label,
                         err
                     );
                     (vec![255, 0, 255, 255], (1, 1))
                 }
             }
         };
+
         let size = wgpu::Extent3d {
             width: dimensions.0,
             height: dimensions.1,
             depth_or_array_layers: 1,
         };
 
-        let desc = texture_descriptor.unwrap_or_else(|| 
-            wgpu::TextureDescriptor {
-                label: Some("diffuse_texture"),
-                size: size,
-                mip_level_count: 1,
-                sample_count: 1,
-                dimension: wgpu::TextureDimension::D2,
-                format: Texture::TEXTURE_FORMAT,
-                usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::RENDER_ATTACHMENT,
-                view_formats: &[],
-            }
-        );
+        let mip_level_count = size.width.min(size.height).ilog2() + 1;
+        log::debug!("Mip level count [{:?}]: {}", label, mip_level_count);
 
-        let texture = device.create_texture(&desc);
+        let texture = graphics.device.create_texture(&wgpu::TextureDescriptor {
+            label: Some(format!("{:?} diffuse blit texture", label).as_str()),
+            size,
+            mip_level_count,
+            sample_count: 1,
+            dimension: wgpu::TextureDimension::D2,
+            format: Texture::TEXTURE_FORMAT,
+            usage: wgpu::TextureUsages::TEXTURE_BINDING
+                | wgpu::TextureUsages::RENDER_ATTACHMENT
+                | wgpu::TextureUsages::COPY_DST
+                | wgpu::TextureUsages::COPY_SRC,
+            view_formats: &[],
+        });
 
         let unpadded_bytes_per_row = 4 * size.width;
         let padded_bytes_per_row = (unpadded_bytes_per_row + wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1) & !(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT - 1);
         debug_assert!(diffuse_rgba.len() >= (unpadded_bytes_per_row * size.height) as usize);
 
         if padded_bytes_per_row == unpadded_bytes_per_row {
-            queue.write_texture(
+            graphics.queue.write_texture(
                 wgpu::TexelCopyTextureInfo {
                     texture: &texture,
                     mip_level: 0,
@@ -278,7 +310,7 @@ impl Texture {
                     .copy_from_slice(&diffuse_rgba[src_start..src_start + src_stride]);
             }
 
-            queue.write_texture(
+            graphics.queue.write_texture(
                 wgpu::TexelCopyTextureInfo {
                     texture: &texture,
                     mip_level: 0,
@@ -304,16 +336,17 @@ impl Texture {
                 address_mode_w: wgpu::AddressMode::ClampToEdge,
                 mag_filter: wgpu::FilterMode::Linear,
                 min_filter: wgpu::FilterMode::Nearest,
-                mipmap_filter: wgpu::FilterMode::Nearest,
+                mipmap_filter: wgpu::FilterMode::Linear,
                 ..Default::default()
             }
         };
 
-        let sampler = device.create_sampler(&sampler_desc);
+        let sampler = graphics.device.create_sampler(&sampler_desc);
 
         let view = texture.create_view(&view_descriptor.unwrap_or_default());
 
         Self {
+            label: label.to_potential_string(),
             texture,
             sampler,
             size,
@@ -328,7 +361,7 @@ impl Texture {
             address_mode_w: wrap.into(),
             mag_filter: wgpu::FilterMode::Linear,
             min_filter: wgpu::FilterMode::Nearest,
-            mipmap_filter: wgpu::FilterMode::Nearest,
+            mipmap_filter: wgpu::FilterMode::Linear,
             ..Default::default()
         }
     }
diff --git a/crates/dropbear-engine/src/utils.rs b/crates/dropbear-engine/src/utils.rs
index 68187d9..0c94d07 100644
--- a/crates/dropbear-engine/src/utils.rs
+++ b/crates/dropbear-engine/src/utils.rs
@@ -263,3 +263,20 @@ macro_rules! resource {
         ::dropbear_engine::utils::ResourceReference::from_euca_uri($path).expect("Invalid euca URI")
     };
 }
+
+/// Helper trait for converting `Option<T: ToString>` to [`Option<String>`] without looking into its contents. 
+pub trait ToPotentialString {
+    /// Converts an [`Option<T>`], where [`T`] can be converted to a [`String`], into an [`Option<String>`].
+    fn to_potential_string(&self) -> Option<String>;
+}
+
+impl<T> ToPotentialString for Option<T> 
+where T: ToString
+{
+    fn to_potential_string(&self) -> Option<String> {
+        match self {
+            None => None,
+            Some(v) => Some(v.to_string())
+        }
+    }
+}
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/scene.rs b/crates/eucalyptus-core/src/scene.rs
index 5a825c7..0edd530 100644
--- a/crates/eucalyptus-core/src/scene.rs
+++ b/crates/eucalyptus-core/src/scene.rs
@@ -266,14 +266,13 @@ impl SceneConfig {
                             if let Some(reference) = &custom.diffuse_texture {
                                 if let Ok(path) = reference.resolve() {
                                     if let Ok(bytes) = std::fs::read(&path) {
-                                        let diffuse = Texture::from_bytes_verbose(
-                                            &graphics.device,
-                                            &graphics.queue,
+                                        let diffuse = Texture::from_bytes_verbose_mipmapped(
+                                            graphics.clone(),
                                             bytes.as_slice(),
                                             None,
                                             None,
-                                            None,
                                             Some(Texture::sampler_from_wrap(custom.wrap_mode)),
+                                            Some(material.name.as_str())
                                         );
                                         let flat_normal = (*ASSET_REGISTRY
                                             .solid_texture_rgba8(
diff --git a/crates/eucalyptus-core/src/scene/scripting.rs b/crates/eucalyptus-core/src/scene/scripting.rs
index 7a91e7c..c76985c 100644
--- a/crates/eucalyptus-core/src/scene/scripting.rs
+++ b/crates/eucalyptus-core/src/scene/scripting.rs
@@ -297,13 +297,12 @@ pub mod jni {
         if let Err(e) = super::shared::switch_to_scene_async(command_buffer, scene_loader, scene_id as u64) {
             eprintln!("Failed to switch scene async: {}", e);
 
-            // Check if it's a premature scene switch error
             if let crate::scripting::native::DropbearNativeError::PrematureSceneSwitch = e {
                 let _ = env.throw_new("com/dropbear/exception/PrematureSceneSwitchException",
-                                      "Cannot switch to scene before it has finished loading");
+                                    "Cannot switch to scene before it has finished loading");
             } else {
                 let _ = env.throw_new("java/lang/RuntimeException",
-                                      format!("Failed to switch scene async: {:?}", e));
+                                    format!("Failed to switch scene async: {:?}", e));
             }
         }
     }
@@ -324,7 +323,7 @@ pub mod jni {
                     Err(e) => {
                         eprintln!("Failed to create Progress object: {:?}", e);
                         let _ = env.throw_new("java/lang/RuntimeException",
-                                              format!("Failed to create Progress object: {:?}", e));
+                                            format!("Failed to create Progress object: {:?}", e));
                         std::ptr::null_mut()
                     }
                 }
@@ -332,7 +331,7 @@ pub mod jni {
             Err(e) => {
                 eprintln!("Failed to get scene load progress: {}", e);
                 let _ = env.throw_new("java/lang/RuntimeException",
-                                      format!("Failed to get scene load progress: {:?}", e));
+                                    format!("Failed to get scene load progress: {:?}", e));
                 std::ptr::null_mut()
             }
         }
@@ -352,7 +351,7 @@ pub mod jni {
             Err(e) => {
                 eprintln!("Failed to get scene load status: {}", e);
                 let _ = env.throw_new("java/lang/RuntimeException",
-                                      format!("Failed to get scene load status: {:?}", e));
+                                    format!("Failed to get scene load status: {:?}", e));
                 -1 as jint
             }
         }
diff --git a/crates/eucalyptus-editor/src/about.rs b/crates/eucalyptus-editor/src/about.rs
index e228d1d..60a4942 100644
--- a/crates/eucalyptus-editor/src/about.rs
+++ b/crates/eucalyptus-editor/src/about.rs
@@ -1,6 +1,5 @@
 //! Used for displaying the Help->About window in the editor.
 
-use dropbear_engine::graphics::FrameGraphicsContext;
 use egui::{CentralPanel};
 use gilrs::{Button, GamepadId};
 use winit::dpi::PhysicalPosition;
@@ -76,7 +75,7 @@ impl Scene for AboutWindow {
         self.window = Some(graphics.window.id());
     }
 
-    fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: FrameGraphicsContext<'a>) {
+    fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {
     }
 
     fn exit(&mut self, _event_loop: &ActiveEventLoop) {}
diff --git a/crates/eucalyptus-editor/src/debug/window.rs b/crates/eucalyptus-editor/src/debug/window.rs
index 52ebd1a..2734d07 100644
--- a/crates/eucalyptus-editor/src/debug/window.rs
+++ b/crates/eucalyptus-editor/src/debug/window.rs
@@ -45,7 +45,7 @@ impl Scene for DebugWindow {
         self.window = Some(graphics.window.id());
     }
 
-    fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: dropbear_engine::graphics::FrameGraphicsContext<'a>) {
+    fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {
     }
 
     fn exit(&mut self, _event_loop: &ActiveEventLoop) {}
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index ed8cb05..3b1cb2f 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -57,6 +57,7 @@ use wgpu::{Color, Extent3d};
 use winit::window::{CursorGrabMode, WindowAttributes};
 use winit::{keyboard::KeyCode, window::Window};
 use winit::dpi::PhysicalSize;
+use dropbear_engine::mipmap::MipMapper;
 use dropbear_engine::pipelines::DropbearShaderPipeline;
 use dropbear_engine::pipelines::shader::MainRenderPipeline;
 use dropbear_engine::pipelines::GlobalsUniform;
@@ -84,6 +85,7 @@ pub struct Editor {
     pub main_render_pipeline: Option<MainRenderPipeline>,
     pub shader_globals: Option<GlobalsUniform>,
     pub collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
+    pub mipmapper: Option<MipMapper>,
 
     pub active_camera: Arc<Mutex<Option<Entity>>>,
 
@@ -248,6 +250,7 @@ impl Editor {
             instance_buffer_cache: HashMap::new(),
             collider_wireframe_geometry_cache: HashMap::new(),
             collider_instance_buffer: None,
+            mipmapper: None,
         })
     }
 
@@ -1323,6 +1326,8 @@ impl Editor {
         self.light_cube_pipeline = Some(LightCubePipeline::new(graphics.clone()));
         self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("editor shader globals")));
         self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone()));
+        // Mipmaps are generated by the engine during texture creation; keep this optional field unused for now.
+        self.mipmapper = None;
 
         self.texture_id = Some((*graphics.texture_id).clone());
         self.window = Some(graphics.window.clone());
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index ceb5aae..403c557 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -7,7 +7,7 @@ use super::*;
 use crate::signal::SignalController;
 use crate::spawn::PendingSpawnController;
 use dropbear_engine::asset::{PointerKind, ASSET_REGISTRY};
-use dropbear_engine::graphics::{FrameGraphicsContext, InstanceRaw};
+use dropbear_engine::graphics::{CommandEncoder, InstanceRaw};
 use dropbear_engine::model::MODEL_CACHE;
 use dropbear_engine::{
     entity::{EntityTransform, MeshRenderer, Transform},
@@ -346,7 +346,7 @@ impl Scene for Editor {
         self.input_state.mouse_delta = None;
     }
 
-    fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, frame_ctx: FrameGraphicsContext<'a>) {
+    fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) {
         self.size = graphics.viewport_texture.size;
         self.texture_id = Some(*graphics.texture_id.clone());
         self.window = Some(graphics.window.clone());
@@ -361,23 +361,7 @@ impl Scene for Editor {
         self.color = clear_color;
         eucalyptus_core::logging::render(&graphics.get_egui_context());
 
-        {
-            let _ = frame_ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                label: Some("editor surface clear pass"),
-                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                    view: &frame_ctx.view,
-                    depth_slice: None,
-                    resolve_target: None,
-                    ops: wgpu::Operations {
-                        load: wgpu::LoadOp::Clear(clear_color),
-                        store: wgpu::StoreOp::Store,
-                    },
-                })],
-                depth_stencil_attachment: None,
-                occlusion_query_set: None,
-                timestamp_writes: None,
-            });
-        }
+        let mut encoder = CommandEncoder::new(graphics.clone(), Some("editor viewport encoder"));
 
         let cam = {
             let c = self.active_camera.lock();
@@ -403,6 +387,37 @@ impl Scene for Editor {
         };
         log_once::debug_once!("Pipeline ready");
 
+        { // ensures clearing of the encoder is done correctly. 
+            let mut encoder = CommandEncoder::new(graphics.clone(), Some("viewport clear render encoder"));
+
+            {
+                let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                    label: Some("viewport clear pass"),
+                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                        view: &graphics.viewport_texture.view,
+                        depth_slice: None,
+                        resolve_target: None,
+                        ops: wgpu::Operations {
+                            load: wgpu::LoadOp::Clear(wgpu::Color {
+                                r: 100.0 / 255.0,
+                                g: 149.0 / 255.0,
+                                b: 237.0 / 255.0,
+                                a: 1.0,
+                            }),
+                            store: wgpu::StoreOp::Store,
+                        },
+                    })],
+                    depth_stencil_attachment: None,
+                    occlusion_query_set: None,
+                    timestamp_writes: None,
+                });
+            }
+
+            if let Err(e) = encoder.submit(graphics.clone()) {
+                log_once::error_once!("{}", e);
+            }
+        }
+
         let lights = {
             let mut lights = Vec::new();
             let mut query = self.world.query::<(&Light, &LightComponent)>();
@@ -484,7 +499,7 @@ impl Scene for Editor {
         }
 
         {
-            let mut render_pass = frame_ctx.encoder
+            let mut render_pass = encoder
                 .begin_render_pass(&wgpu::RenderPassDescriptor {
                     label: Some("light cube render pass"),
                     color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -521,6 +536,9 @@ impl Scene for Editor {
                 }
             }
         }
+
+                
+
         if let Some(lcp) = &self.light_cube_pipeline {
             for (model, instance_buffer, instance_count) in prepared_models {
                 let globals_bind_group = &self
@@ -529,7 +547,7 @@ impl Scene for Editor {
                     .expect("Shader globals not initialised")
                     .bind_group;
 
-                let mut render_pass = frame_ctx.encoder
+                let mut render_pass = encoder
                     .begin_render_pass(&wgpu::RenderPassDescriptor {
                         label: Some("model render pass"),
                         color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -579,7 +597,7 @@ impl Scene for Editor {
 
             if show_hitboxes {
                 if let Some(collider_pipeline) = &self.collider_wireframe_pipeline {
-                    let mut render_pass = frame_ctx.encoder
+                    let mut render_pass = encoder
                         .begin_render_pass(&wgpu::RenderPassDescriptor {
                             label: Some("model render pass"),
                             color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -699,6 +717,9 @@ impl Scene for Editor {
                     }
                 }
             }
+            if let Err(e) = encoder.submit(graphics.clone()) {
+                log_once::error_once!("{}", e);
+            }
         }
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/settings/editor.rs b/crates/eucalyptus-editor/src/editor/settings/editor.rs
index 051a931..ab01897 100644
--- a/crates/eucalyptus-editor/src/editor/settings/editor.rs
+++ b/crates/eucalyptus-editor/src/editor/settings/editor.rs
@@ -213,7 +213,7 @@ impl Scene for EditorSettingsWindow {
         self.window = Some(graphics.window.id());
     }
 
-    fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: dropbear_engine::graphics::FrameGraphicsContext<'a>) {
+    fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {
     }
 
     fn exit(&mut self, _event_loop: &ActiveEventLoop) {
diff --git a/crates/eucalyptus-editor/src/editor/settings/project.rs b/crates/eucalyptus-editor/src/editor/settings/project.rs
index 15593bd..368af68 100644
--- a/crates/eucalyptus-editor/src/editor/settings/project.rs
+++ b/crates/eucalyptus-editor/src/editor/settings/project.rs
@@ -183,7 +183,7 @@ impl Scene for ProjectSettingsWindow {
         self.window = Some(graphics.window.id());
     }
 
-    fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: dropbear_engine::graphics::FrameGraphicsContext<'a>) {
+    fn render<'a>(&mut self, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {
     }
 
     fn exit(&mut self, _event_loop: &ActiveEventLoop) {
diff --git a/crates/eucalyptus-editor/src/menu.rs b/crates/eucalyptus-editor/src/menu.rs
index caf6fec..21c9841 100644
--- a/crates/eucalyptus-editor/src/menu.rs
+++ b/crates/eucalyptus-editor/src/menu.rs
@@ -1,5 +1,5 @@
 use anyhow::{Context, anyhow};
-use dropbear_engine::{DropbearWindowBuilder, future::{FutureHandle, FutureQueue}, graphics::FrameGraphicsContext, input::{Controller, Keyboard, Mouse}, scene::{Scene, SceneCommand}};
+use dropbear_engine::{DropbearWindowBuilder, future::{FutureHandle, FutureQueue}, input::{Controller, Keyboard, Mouse}, scene::{Scene, SceneCommand}};
 use egui::{self, FontId, Frame, RichText};
 use egui_toast::{ToastOptions, Toasts};
 use eucalyptus_core::config::ProjectConfig;
@@ -243,7 +243,7 @@ impl Scene for MainMenu {
 
     fn update(&mut self, _dt: f32, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {}
 
-    fn render<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: FrameGraphicsContext<'a>) {
+    fn render<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {
         #[allow(clippy::collapsible_if)]
         if let Some(handle) = self.project_creation_handle.as_ref() {
             if let Some(result) = graphics
diff --git a/crates/eucalyptus-editor/src/signal.rs b/crates/eucalyptus-editor/src/signal.rs
index 1302e58..f03b7e3 100644
--- a/crates/eucalyptus-editor/src/signal.rs
+++ b/crates/eucalyptus-editor/src/signal.rs
@@ -800,14 +800,13 @@ impl SignalController for Editor {
                                         let path = resolve_editor_path(&uri);
 
                                         if let Ok(bytes) = std::fs::read(&path) {
-                                            let diffuse = Texture::from_bytes_verbose(
-                                                &graphics.device, 
-                                                &graphics.queue, 
-                                                &bytes, 
-                                                None, 
+                                            let diffuse = Texture::from_bytes_verbose_mipmapped(
+                                                graphics.clone(),
+                                                &bytes,
                                                 None,
                                                 None,
-                                                Some(Texture::sampler_from_wrap(wrap_mode))
+                                                Some(Texture::sampler_from_wrap(wrap_mode)),
+                                                Some(mat_name.as_str())
                                             );
                                             let flat_normal = (*dropbear_engine::asset::ASSET_REGISTRY
                                                 .solid_texture_rgba8(
@@ -856,14 +855,13 @@ impl SignalController for Editor {
                     }
                 };
 
-                let diffuse = Texture::from_bytes_verbose(
-                    &graphics.device, 
-                    &graphics.queue, 
-                    &bytes, 
-                    None, 
+                let diffuse = Texture::from_bytes_verbose_mipmapped(
+                    graphics.clone(),
+                    &bytes,
                     None,
                     None,
-                    Some(Texture::sampler_from_wrap(wrap_mode.clone()))
+                    Some(Texture::sampler_from_wrap(wrap_mode.clone())),
+                    Some(target_material)
                 );
                 let flat_normal = (*dropbear_engine::asset::ASSET_REGISTRY
                     .solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]))
@@ -911,14 +909,13 @@ impl SignalController for Editor {
                             let path = resolve_editor_path(&uri);
 
                             if let Ok(bytes) = std::fs::read(&path) {
-                                let diffuse = Texture::from_bytes_verbose(
-                                    &graphics.device, 
-                                    &graphics.queue, 
-                                    &bytes, 
-                                    None, 
+                                let diffuse = Texture::from_bytes_verbose_mipmapped(
+                                    graphics.clone(),
+                                    &bytes,
                                     None,
                                     None,
-                                    Some(Texture::sampler_from_wrap(wrap_mode.clone()))
+                                    Some(Texture::sampler_from_wrap(wrap_mode.clone())),
+                                    Some(target_material)
                                 );
                                 material.diffuse_texture = diffuse;
                                 material.bind_group = Material::create_bind_group(
diff --git a/crates/eucalyptus-editor/src/spawn.rs b/crates/eucalyptus-editor/src/spawn.rs
index b5572c3..9b1e98c 100644
--- a/crates/eucalyptus-editor/src/spawn.rs
+++ b/crates/eucalyptus-editor/src/spawn.rs
@@ -393,14 +393,13 @@ async fn load_renderer_from_serialized(
                     if let Ok(path) = reference.resolve() {
                         match std::fs::read(&path) {
                             Ok(bytes) => {
-                                let diffuse = Texture::from_bytes_verbose(
-                                    &graphics.device, 
-                                    &graphics.queue, 
-                                    &bytes, 
-                                    None, 
+                                let diffuse = Texture::from_bytes_verbose_mipmapped(
+                                    graphics.clone(),
+                                    &bytes,
                                     None,
                                     None,
-                                    Some(Texture::sampler_from_wrap(custom.wrap_mode))
+                                    Some(Texture::sampler_from_wrap(custom.wrap_mode)),
+                                    Some(material.name.as_str()),
                                 );
                                 let flat_normal = (*ASSET_REGISTRY
                                     .solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]))
diff --git a/crates/eucalyptus-editor/src/stats.rs b/crates/eucalyptus-editor/src/stats.rs
index 2106c99..eb39a8a 100644
--- a/crates/eucalyptus-editor/src/stats.rs
+++ b/crates/eucalyptus-editor/src/stats.rs
@@ -1,7 +1,6 @@
 use std::{collections::VecDeque, time::Instant};
 
 use dropbear_engine::WGPU_BACKEND;
-use dropbear_engine::graphics::FrameGraphicsContext;
 use egui::{Color32, Context, RichText, Ui};
 use egui_plot::{Legend, Line, Plot, PlotPoints};
 use dropbear_engine::scene::Scene;
@@ -336,7 +335,7 @@ impl Scene for NerdStats {
     fn update(&mut self, dt: f32, _graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {
         self.record_stats(dt, self.entity_count);
     }
-    fn render<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, _frame_ctx: FrameGraphicsContext<'a>) {
+    fn render<'a>(&mut self, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>) {
         self.show_window(&graphics.get_egui_context());
     }
     fn exit(&mut self, _event_loop: &ActiveEventLoop) {}
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 91475b5..cd2e8d8 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -2,19 +2,20 @@ use std::sync::Arc;
 
 use std::collections::HashMap;
 use dropbear_engine::pipelines::DropbearShaderPipeline;
+use dropbear_engine::graphics::CommandEncoder;
 use eucalyptus_core::egui::CentralPanel;
 use eucalyptus_core::physics::collider::ColliderGroup;
 use eucalyptus_core::physics::collider::ColliderShapeKey;
 use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw;
 use glam::{DMat4, DQuat, DVec3, Quat};
 use hecs::Entity;
-use wgpu::Color;
+use wgpu::{Color};
 use wgpu::util::DeviceExt;
 use winit::event_loop::ActiveEventLoop;
 use dropbear_engine::camera::Camera;
 use dropbear_engine::buffer::ResizableBuffer;
 use dropbear_engine::entity::{EntityTransform, MeshRenderer, Transform};
-use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext, FrameGraphicsContext};
+use dropbear_engine::graphics::{InstanceRaw, SharedGraphicsContext};
 use dropbear_engine::lighting::{Light, LightComponent};
 use dropbear_engine::lighting::MAX_LIGHTS;
 use dropbear_engine::model::{DrawLight, DrawModel, ModelId, MODEL_CACHE};
@@ -456,7 +457,16 @@ impl Scene for PlayMode {
         self.input_state.mouse_delta = None;
     }
 
-    fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, frame_ctx: FrameGraphicsContext<'a>) {
+    fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) {
+        let clear_color = Color {
+            r: 100.0 / 255.0,
+            g: 149.0 / 255.0,
+            b: 237.0 / 255.0,
+            a: 1.0,
+        };
+
+        let mut encoder = CommandEncoder::new(graphics.clone(), Some("runtime viewport encoder"));
+
         let Some(active_camera) = self.active_camera else {
             return;
         };
@@ -476,12 +486,36 @@ impl Scene for PlayMode {
         };
         log_once::debug_once!("Pipeline ready");
 
-        let clear_color = Color {
-            r: 100.0 / 255.0,
-            g: 149.0 / 255.0,
-            b: 237.0 / 255.0,
-            a: 1.0,
-        };
+        { // ensures clearing of the encoder is done correctly. 
+            let mut encoder = dropbear_engine::graphics::CommandEncoder::new(graphics.clone(), Some("viewport clear render encoder"));
+
+            {
+                let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                    label: Some("viewport clear pass"),
+                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                        view: &graphics.viewport_texture.view,
+                        depth_slice: None,
+                        resolve_target: None,
+                        ops: wgpu::Operations {
+                            load: wgpu::LoadOp::Clear(wgpu::Color {
+                                r: 100.0 / 255.0,
+                                g: 149.0 / 255.0,
+                                b: 237.0 / 255.0,
+                                a: 1.0,
+                            }),
+                            store: wgpu::StoreOp::Store,
+                        },
+                    })],
+                    depth_stencil_attachment: None,
+                    occlusion_query_set: None,
+                    timestamp_writes: None,
+                });
+            }
+
+            if let Err(e) = encoder.submit(graphics.clone()) {
+                log_once::error_once!("{}", e);
+            }
+        }
 
         let lights = {
             let mut lights = Vec::new();
@@ -492,15 +526,15 @@ impl Scene for PlayMode {
             lights
         };
 
-        if let Some(globals) = &mut self.shader_globals {
-            let enabled_count = lights
-                .iter()
-                .filter(|(_, comp)| comp.enabled)
-                .take(MAX_LIGHTS)
-                .count() as u32;
-            globals.set_num_lights(enabled_count);
-            globals.write(&graphics.queue);
-        }
+            if let Some(globals) = &mut self.shader_globals {
+                let enabled_count = lights
+                    .iter()
+                    .filter(|(_, comp)| comp.enabled)
+                    .take(MAX_LIGHTS)
+                    .count() as u32;
+                globals.set_num_lights(enabled_count);
+                globals.write(&graphics.queue);
+            }
 
         let renderers = {
             let mut renderers = Vec::new();
@@ -564,25 +598,7 @@ impl Scene for PlayMode {
         }
 
         {
-            let _ = frame_ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                label: Some("runtime surface clear pass"),
-                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                    view: &frame_ctx.view,
-                    depth_slice: None,
-                    resolve_target: None,
-                    ops: wgpu::Operations {
-                        load: wgpu::LoadOp::Clear(clear_color),
-                        store: wgpu::StoreOp::Store,
-                    },
-                })],
-                depth_stencil_attachment: None,
-                occlusion_query_set: None,
-                timestamp_writes: None,
-            });
-        }
-
-        {
-            let mut render_pass = frame_ctx.encoder
+            let mut render_pass = encoder
                 .begin_render_pass(&wgpu::RenderPassDescriptor {
                     label: Some("light cube render pass"),
                     color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -620,53 +636,51 @@ impl Scene for PlayMode {
             }
         }
 
-        for (model, instance_buffer, instance_count) in prepared_models {
-            let light_bind_group = self
-                .light_cube_pipeline
-                .as_ref()
-                .expect("Light cube pipeline not initialised")
-                .bind_group();
-
-            let globals_bind_group = &self
-                .shader_globals
-                .as_ref()
-                .expect("Shader globals not initialised")
-                .bind_group;
-
-            let mut render_pass = frame_ctx.encoder
-                .begin_render_pass(&wgpu::RenderPassDescriptor {
-                    label: Some("model render pass"),
-                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                        view: &graphics.viewport_texture.view,
-                        depth_slice: None,
-                        resolve_target: None,
-                        ops: wgpu::Operations {
-                            load: wgpu::LoadOp::Load,
-                            store: wgpu::StoreOp::Store,
-                        },
-                    })],
-                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
-                        view: &graphics.depth_texture.view,
-                        depth_ops: Some(wgpu::Operations {
-                            load: wgpu::LoadOp::Load,
-                            store: wgpu::StoreOp::Store,
+        // model rendering
+        if let Some(lcp) = &self.light_cube_pipeline {
+            for (model, instance_buffer, instance_count) in prepared_models {
+                let globals_bind_group = &self
+                    .shader_globals
+                    .as_ref()
+                    .expect("Shader globals not initialised")
+                    .bind_group;
+
+                let mut render_pass = encoder
+                    .begin_render_pass(&wgpu::RenderPassDescriptor {
+                        label: Some("model render pass"),
+                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                            view: &graphics.viewport_texture.view,
+                            depth_slice: None,
+                            resolve_target: None,
+                            ops: wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            },
+                        })],
+                        depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                            view: &graphics.depth_texture.view,
+                            depth_ops: Some(wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            }),
+                            stencil_ops: None,
                         }),
-                        stencil_ops: None,
-                    }),
-                    occlusion_query_set: None,
-                    timestamp_writes: None,
-                });
-            render_pass.set_pipeline(pipeline.pipeline());
-            render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
-            render_pass.set_bind_group(4, globals_bind_group, &[]);
-            render_pass.draw_model_instanced(
-                &model,
-                0..instance_count,
-                &camera.bind_group,
-                light_bind_group,
-            );
+                        occlusion_query_set: None,
+                        timestamp_writes: None,
+                    });
+                render_pass.set_pipeline(pipeline.pipeline());
+                render_pass.set_vertex_buffer(1, instance_buffer.slice(..));
+                render_pass.set_bind_group(4, globals_bind_group, &[]);
+                render_pass.draw_model_instanced(
+                    &model,
+                    0..instance_count,
+                    &camera.bind_group,
+                    lcp.bind_group(),
+                );
+            }
         }
 
+        // collider pipeline
         {
             let show_hitboxes = self
                 .current_scene
@@ -682,7 +696,7 @@ impl Scene for PlayMode {
 
             if show_hitboxes {
                 if let Some(collider_pipeline) = &self.collider_wireframe_pipeline {
-                    let mut render_pass = frame_ctx.encoder
+                    let mut render_pass = encoder
                         .begin_render_pass(&wgpu::RenderPassDescriptor {
                             label: Some("model render pass"),
                             color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -802,6 +816,9 @@ impl Scene for PlayMode {
                     }
                 }
             }
+            if let Err(e) = encoder.submit(graphics.clone()) {
+                log_once::error_once!("{}", e);
+            }
         }
     }