kitgit

tirbofish/dropbear · commit

51530a7586cbce78e70f9471aca281317e6c5064

feature: skybox on redback-runtime works todo: animations now and user defined skyboxes and reflections.

Unverified

Thribhu K <4tkbytes@pm.me> · 2026-02-09 01:55

view full diff

diff --git a/Cargo.toml b/Cargo.toml
index aed3628..18b21b4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -81,6 +81,9 @@ thiserror = "2.0"
 tempfile = "3.24"
 combine = "4.6"
 glyphon = { git = "https://github.com/grovesNL/glyphon", rev = "9dd9376" }
+puffin = "0.19"
+bitflags = "2.10"
+features = "0.10"
 
 [workspace.dependencies.image]
 version = "0.24"
diff --git a/crates/dropbear-engine/Cargo.toml b/crates/dropbear-engine/Cargo.toml
index b972591..094c9cf 100644
--- a/crates/dropbear-engine/Cargo.toml
+++ b/crates/dropbear-engine/Cargo.toml
@@ -45,6 +45,9 @@ typetag.workspace = true
 postcard.workspace = true
 pollster.workspace = true
 image.workspace = true
+puffin.workspace = true
+bitflags.workspace = true
+
 #yakui-wgpu.workspace = true
 #yakui.workspace = true
 
diff --git a/crates/dropbear-engine/src/features.rs b/crates/dropbear-engine/src/features.rs
new file mode 100644
index 0000000..b905c5e
--- /dev/null
+++ b/crates/dropbear-engine/src/features.rs
@@ -0,0 +1,301 @@
+// Copyright ⓒ 2017 Fletcher Nichol and/or applicable contributors.
+//
+// Licensed under the Apache License, Version 2.0 (see LICENSE-APACHE or
+// <http://www.apache.org/licenses/LICENSE-2.0>) or the MIT license (see<LICENSE-MIT or
+// <http://opensource.org/licenses/MIT>, at your option. This file may not be copied, modified, or
+// distributed except according to those terms.
+
+//! `features` is a small library that implements runtime [feature toggles][fowler_toggles] for
+//! your library or program allowing behavior to be changed on boot or dynamically at runtime using
+//! the same compiled binary artifact. This is different from cargo's [feature][cargo_feature]
+//! support which uses conditional compilation.
+//!
+//! At its core, it is a macro (`features!`) that takes a collection of feature flag names which it
+//! uses to generate a module containing a function to enable a feature toggle (`::enable()`), a
+//! function to disable a feature toggle (`::disable()`) and a function to check if a feature
+//! toggle is enabled (`::is_enabled()`).
+//!
+//! ## Example
+//!
+//! Basic example:
+//!
+//! ```
+//! #[macro_use]
+//! extern crate bitflags;
+//! #[macro_use]
+//! extern crate features;
+//!
+//! features! {
+//!     pub mod feature {
+//!         const Alpha = 0b00000001,
+//!         const Beta = 0b00000010
+//!     }
+//! }
+//!
+//! fn main() {
+//!     assert_eq!(false, feature::is_enabled(feature::Alpha));
+//!     assert_eq!(false, feature::is_enabled(feature::Beta));
+//!
+//!     feature::enable(feature::Beta);
+//!     assert_eq!(false, feature::is_enabled(feature::Alpha));
+//!     assert_eq!(true, feature::is_enabled(feature::Beta));
+//! }
+//! ```
+//!
+//! Multiple feature sets:
+//!
+//! ```
+//! #[macro_use]
+//! extern crate bitflags;
+//! #[macro_use]
+//! extern crate features;
+//!
+//! features! {
+//!     pub mod ux {
+//!         const JsonOutput = 0b10000000,
+//!         const VerboseOutput = 0b01000000
+//!     }
+//! }
+//!
+//! features! {
+//!     pub mod srv {
+//!         const Http2Downloading = 0b10000000,
+//!         const BitTorrentDownloading = 0b01000000
+//!     }
+//! }
+//!
+//! fn main() {
+//!     // Parse CLI args, environment, read config file etc...
+//!     srv::enable(srv::BitTorrentDownloading);
+//!     ux::enable(ux::JsonOutput);
+//!
+//!     if srv::is_enabled(srv::Http2Downloading) {
+//!         println!("Downloading via http2...");
+//!     } else if srv::is_enabled(srv::BitTorrentDownloading) {
+//!         println!("Downloading via bit torrent...");
+//!     } else {
+//!         println!("Downloading the old fashioned way...");
+//!     }
+//!
+//!     if ux::is_enabled(ux::VerboseOutput) {
+//!         println!("COOL");
+//!     }
+//! }
+//! ```
+//!
+//! ## Feature Toggle References
+//!
+//! Here are some articles and projects which insipred the implementation of `features`:
+//!
+//! * [Feature Toggles](https://martinfowler.com/articles/feature-toggles.html) (Martin Fowler's
+//! blog)
+//! * [Using Feature Flags to Ship Changes with
+//! Confidence](https://blog.travis-ci.com/2014-03-04-use-feature-flags-to-ship-changes-with-confidence/)
+//! (TravisCI's blog)
+//! * [Feature Toggles are one of the worst kinds of Technical
+//! Debt](http://swreflections.blogspot.ca/2014/08/feature-toggles-are-one-of-worst-kinds.html)
+//! (excellent cautionary tale and warning)
+//! * [Feature toggle](https://en.wikipedia.org/wiki/Feature_toggle) (Wikipedia article)
+//! * [Ruby feature gem](https://github.com/mgsnova/feature) (nice prior art)
+//!
+//! [fowler_toggles]: https://martinfowler.com/articles/feature-toggles.html
+//! [cargo_feature]: http://doc.crates.io/manifest.html#the-features-section
+
+/// The `features!` macro generates a module to contain all feature toggling logic.
+///
+/// # Examples
+///
+/// Basic example:
+///
+/// ```
+/// #[macro_use]
+/// extern crate bitflags;
+/// #[macro_use]
+/// extern crate features;
+///
+/// features! {
+///     pub mod feature {
+///         const Alpha = 0b00000001,
+///         const Beta = 0b00000010
+///     }
+/// }
+///
+/// fn main() {
+///     assert_eq!(false, feature::is_enabled(feature::Alpha));
+///     assert_eq!(false, feature::is_enabled(feature::Beta));
+///
+///     feature::enable(feature::Beta);
+///     assert_eq!(false, feature::is_enabled(feature::Alpha));
+///     assert_eq!(true, feature::is_enabled(feature::Beta));
+/// }
+/// ```
+///
+/// Multiple feature sets:
+///
+/// ```
+/// #[macro_use]
+/// extern crate bitflags;
+/// #[macro_use]
+/// extern crate features;
+///
+/// features! {
+///     pub mod ux {
+///         const JsonOutput = 0b10000000,
+///         const VerboseOutput = 0b01000000
+///     }
+/// }
+///
+/// features! {
+///     pub mod srv {
+///         const Http2Downloading = 0b10000000,
+///         const BitTorrentDownloading = 0b01000000
+///     }
+/// }
+///
+/// fn main() {
+///     // Parse CLI args, environment, read config file etc...
+///     srv::enable(srv::BitTorrentDownloading);
+///     ux::enable(ux::JsonOutput);
+///
+///     if srv::is_enabled(srv::Http2Downloading) {
+///         println!("Downloading via http2...");
+///     } else if srv::is_enabled(srv::BitTorrentDownloading) {
+///         println!("Downloading via bit torrent...");
+///     } else {
+///         println!("Downloading the old fashioned way...");
+///     }
+///
+///     if ux::is_enabled(ux::VerboseOutput) {
+///         println!("COOL");
+///     }
+/// }
+/// ```
+#[macro_export]
+macro_rules! features {
+    (mod $mod_name:ident {
+        $($(#[$flag_attr:meta])* const $flag:ident = $value:expr),+
+    }) => {
+        #[allow(non_upper_case_globals)]
+        mod $mod_name {
+            use $crate::features;
+            features! {
+                @_impl mod $mod_name {
+                    $($(#[$flag_attr])* const $flag = $value),+
+                }
+            }
+        }
+    };
+    (pub mod $mod_name:ident {
+        $($(#[$flag_attr:meta])* const $flag:ident = $value:expr),+
+    }) => {
+        #[allow(non_upper_case_globals)]
+        pub mod $mod_name {
+            use $crate::features;
+            features! {
+                @_impl mod $mod_name {
+                    $($(#[$flag_attr])* const $flag = $value),+
+                }
+            }
+        }
+    };
+    (@_impl mod $mod_name:ident {
+        $($(#[$flag_attr:meta])* const $flag:ident = $value:expr),+
+    }) => {
+        use std::sync::atomic;
+        use bitflags::bitflags;
+
+        bitflags! {
+            pub struct Flags: usize {
+                $(
+                    $(#[$flag_attr])* const $flag = $value;
+                )+
+            }
+        }
+        $(
+            pub const $flag: Flags = Flags::$flag;
+        )+
+
+        static FEATURES: atomic::AtomicUsize = atomic::AtomicUsize::new(0);
+
+        #[allow(dead_code)]
+        pub fn enable(flag: Flags) {
+            let mut flags = Flags::from_bits_truncate(FEATURES.load(atomic::Ordering::Relaxed));
+            flags.insert(flag);
+            FEATURES.store(flags.bits(), atomic::Ordering::Relaxed);
+        }
+
+        #[allow(dead_code)]
+        pub fn disable(flag: Flags) {
+            let mut flags = Flags::from_bits_truncate(FEATURES.load(atomic::Ordering::Relaxed));
+            flags.remove(flag);
+            FEATURES.store(flags.bits(), atomic::Ordering::Relaxed);
+        }
+
+        #[allow(dead_code)]
+        pub fn is_enabled(flag: Flags) -> bool {
+            flags().contains(flag)
+        }
+
+        #[allow(dead_code)]
+        pub fn flags() -> Flags {
+            Flags::from_bits_truncate(FEATURES.load(atomic::Ordering::Relaxed))
+        }
+    };
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn enabling() {
+        features! {
+            pub mod f {
+                const Alpha = 0b00000001,
+                const Beta = 0b00000010
+            }
+        }
+
+        assert_eq!(false, f::is_enabled(f::Alpha));
+        assert_eq!(false, f::is_enabled(f::Beta));
+
+        f::enable(f::Alpha);
+        assert_eq!(true, f::is_enabled(f::Alpha));
+        assert_eq!(false, f::is_enabled(f::Beta));
+
+        // Enable again
+        f::enable(f::Alpha);
+        assert_eq!(true, f::is_enabled(f::Alpha));
+        assert_eq!(false, f::is_enabled(f::Beta));
+
+        f::enable(f::Beta);
+        assert_eq!(true, f::is_enabled(f::Alpha));
+        assert_eq!(true, f::is_enabled(f::Beta));
+    }
+
+    #[test]
+    fn disabling() {
+        features! {
+            pub mod f {
+                const Cool = 0b01000000,
+                const Beans = 0b10000000
+            }
+        }
+
+        f::enable(f::Cool);
+        f::enable(f::Beans);
+        assert_eq!(true, f::is_enabled(f::Cool));
+        assert_eq!(true, f::is_enabled(f::Beans));
+
+        f::disable(f::Cool);
+        assert_eq!(false, f::is_enabled(f::Cool));
+        assert_eq!(true, f::is_enabled(f::Beans));
+
+        // Disable again
+        f::disable(f::Cool);
+        assert_eq!(false, f::is_enabled(f::Cool));
+        assert_eq!(true, f::is_enabled(f::Beans));
+
+        f::disable(f::Beans);
+        assert_eq!(false, f::is_enabled(f::Cool));
+        assert_eq!(false, f::is_enabled(f::Beans));
+    }
+}
\ No newline at end of file
diff --git a/crates/dropbear-engine/src/lib.rs b/crates/dropbear-engine/src/lib.rs
index ed8bb82..a2de0a3 100644
--- a/crates/dropbear-engine/src/lib.rs
+++ b/crates/dropbear-engine/src/lib.rs
@@ -19,6 +19,14 @@ pub mod texture;
 pub mod pipelines;
 pub mod mipmap;
 pub mod sky;
+pub mod features;
+
+features! {
+    pub mod build {
+        const Debug = 0b00000001,
+        const Release = 0b00000000
+    }
+}
 
 pub static WGPU_BACKEND: OnceLock<String> = OnceLock::new();
 pub const PHYSICS_STEP_RATE: u32 = 120;
@@ -234,7 +242,7 @@ Hardware:
             present_mode: surface_caps.present_modes[0],
             // alpha_mode: surface_caps.alpha_modes[0],
             alpha_mode: wgpu::CompositeAlphaMode::Auto,
-            view_formats: vec![],
+            view_formats: vec![surface_format.add_srgb_suffix()],
             desired_maximum_frame_latency: 2,
         };
 
@@ -343,7 +351,7 @@ Hardware:
         let hdr = Arc::new(RwLock::new(HdrPipeline::new(
             &device,
             &config,
-            Texture::TEXTURE_FORMAT,
+            config.format.add_srgb_suffix(),
         )));
 
         // let yakui_renderer = Arc::new(Mutex::new(yakui_wgpu::YakuiWgpu::new(
@@ -496,7 +504,11 @@ Hardware:
 
         let view = output
             .texture
-            .create_view(&wgpu::TextureViewDescriptor::default());
+            .create_view(&wgpu::TextureViewDescriptor {
+                label: Some("surface texture view descriptor"),
+                format: Some(self.config.read().format.add_srgb_suffix()),
+                ..Default::default()
+            });
 
         { // ensures clearing of the encoder is done correctly. 
             let mut encoder = CommandEncoder::new(graphics.clone(), Some("surface clear render encoder"));
@@ -902,6 +914,10 @@ impl App {
     /// Creates a new instance of the application. It only sets the default for the struct + the
     /// window config.
     fn new(app_data: AppInfo, future_queue: Option<Arc<FutureQueue>>) -> Self {
+        if build::is_enabled(build::Debug) {
+            puffin::set_scopes_on(true);
+        }
+
         let instance = Arc::new(Instance::new(&wgpu::InstanceDescriptor {
             backends: wgpu::Backends::PRIMARY,
             ..Default::default()
@@ -1073,6 +1089,8 @@ impl ApplicationHandler for App {
             WindowEvent::RedrawRequested => {
                 self.future_queue.poll();
 
+                puffin::GlobalProfiler::lock().new_frame();
+
                 let frame_start = Instant::now();
 
                 let active_handlers = state.scene_manager.get_active_input_handlers();
diff --git a/crates/dropbear-engine/src/pipelines/globals.rs b/crates/dropbear-engine/src/pipelines/globals.rs
index 6d63b61..28b0077 100644
--- a/crates/dropbear-engine/src/pipelines/globals.rs
+++ b/crates/dropbear-engine/src/pipelines/globals.rs
@@ -19,7 +19,7 @@ impl Default for Globals {
     fn default() -> Self {
         Self {
             num_lights: 0,
-            ambient_strength: 0.1,
+            ambient_strength: 0.8,
             _padding: [0; 2],
         }
     }
diff --git a/crates/dropbear-engine/src/sky.rs b/crates/dropbear-engine/src/sky.rs
index 8d50886..d0c7456 100644
--- a/crates/dropbear-engine/src/sky.rs
+++ b/crates/dropbear-engine/src/sky.rs
@@ -5,6 +5,8 @@ use image::codecs::hdr::HdrDecoder;
 use crate::graphics::SharedGraphicsContext;
 use crate::pipelines::{create_render_pipeline_ex};
 
+pub const DEFAULT_SKY_TEXTURE: &[u8] = include_bytes!("../../../resources/textures/kloofendal_48d_partly_cloudy_puresky_4k.hdr");
+
 pub struct CubeTexture {
     texture: wgpu::Texture,
     sampler: wgpu::Sampler,
diff --git a/crates/dropbear-engine/src/texture.rs b/crates/dropbear-engine/src/texture.rs
index 8922af3..44c6ab5 100644
--- a/crates/dropbear-engine/src/texture.rs
+++ b/crates/dropbear-engine/src/texture.rs
@@ -207,7 +207,7 @@ impl Texture {
             mip_level_count: 1, // leave me alone
             sample_count: 1,
             dimension: wgpu::TextureDimension::D2,
-            format: Texture::TEXTURE_FORMAT,
+            format: config.format.add_srgb_suffix(),
             usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
             view_formats: &[],
         };
diff --git a/crates/eucalyptus-editor/src/editor/dock.rs b/crates/eucalyptus-editor/src/editor/dock.rs
index 2af4bb5..b09122e 100644
--- a/crates/eucalyptus-editor/src/editor/dock.rs
+++ b/crates/eucalyptus-editor/src/editor/dock.rs
@@ -215,21 +215,6 @@ impl<'a> TabViewer for EditorTabViewer<'a> {
         match tab {
             EditorTab::Viewport => {
                 log_once::debug_once!("Viewport focused");
-                // ------------------- Template for querying active camera -----------------
-                // if let Some(active_camera) = self.active_camera {
-                //     if let Ok(mut q) = self.world.query_one::<(&Camera, &CameraComponent, Option<&CameraFollowTarget>)>(*active_camera) {
-                //         if let Some((camera, component, follow_target)) = q.get() {
-
-                //         } else {
-                //             log_once::warn_once!("Unable to fetch the query result of camera: {:?}", active_camera)
-                //         }
-                //     } else {
-                //         log_once::warn_once!("Unable to query camera, component and option<camerafollowtarget> for active camera: {:?}", active_camera);
-                //     }
-                // } else {
-                //     log_once::warn_once!("No active camera found");
-                // }
-                // -------------------------------------------------------------------------
 
                 let available_rect = ui.available_rect_before_wrap();
                 let available_size = available_rect.size();
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index 5ad6d8d..5303b74 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -62,7 +62,7 @@ use dropbear_engine::mipmap::MipMapper;
 use dropbear_engine::pipelines::{create_render_pipeline, DropbearShaderPipeline};
 use dropbear_engine::pipelines::shader::MainRenderPipeline;
 use dropbear_engine::pipelines::GlobalsUniform;
-use dropbear_engine::sky::{HdrLoader, SkyPipeline};
+use dropbear_engine::sky::{HdrLoader, SkyPipeline, DEFAULT_SKY_TEXTURE};
 use eucalyptus_core::physics::collider::{ColliderShapeKey, WireframeGeometry};
 use eucalyptus_core::physics::collider::shader::ColliderInstanceRaw;
 use eucalyptus_core::physics::collider::shader::ColliderWireframePipeline;
@@ -1353,18 +1353,16 @@ 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());
         self.is_world_loaded.mark_rendering_loaded();
 
-        let sky_bytes = include_bytes!("../../../../resources/textures/kloofendal_48d_partly_cloudy_puresky_4k.hdr");
         let sky_texture = HdrLoader::from_equirectangular_bytes(
             &graphics.device,
             &graphics.queue,
-            sky_bytes,
+            DEFAULT_SKY_TEXTURE,
             1080,
             Some("sky texture")
         );
diff --git a/crates/kino-ui/src/lib.rs b/crates/kino-ui/src/lib.rs
index 62ac460..ce54635 100644
--- a/crates/kino-ui/src/lib.rs
+++ b/crates/kino-ui/src/lib.rs
@@ -303,7 +303,7 @@ impl KinoState {
                         device,
                         queue,
                         self.renderer.texture_bind_group_layout(),
-                        self.renderer.format,
+                        self.renderer.texture_format(),
                     );
                     return self.assets.add_texture_with_label(label, fallback);
                 }
@@ -323,7 +323,7 @@ impl KinoState {
             raw.as_ref(),
             width,
             height,
-            self.renderer.format,
+            self.renderer.texture_format(),
         );
         self.assets.add_texture_with_label(label, texture)
     }
diff --git a/crates/kino-ui/src/rendering.rs b/crates/kino-ui/src/rendering.rs
index 90c9fbd..2d74b9d 100644
--- a/crates/kino-ui/src/rendering.rs
+++ b/crates/kino-ui/src/rendering.rs
@@ -15,6 +15,7 @@ pub struct KinoWGPURenderer {
     pipeline: KinoRendererPipeline,
     default_texture: texture::Texture,
     pub format: wgpu::TextureFormat,
+    texture_format: wgpu::TextureFormat,
     pub size: Vec2,
     pub text: KinoTextRenderer,
 
@@ -31,6 +32,7 @@ impl KinoWGPURenderer {
     ) -> Self {
         log::debug!("Creating KinoWGPURenderer");
         let pipeline = KinoRendererPipeline::new(device, surface_format);
+        let texture_format = wgpu::TextureFormat::Rgba8UnormSrgb;
 
         let camera_buffer = device.create_buffer_init(&BufferInitDescriptor {
             label: None,
@@ -54,7 +56,12 @@ impl KinoWGPURenderer {
             }],
         });
 
-        let default_texture = texture::Texture::create_default(&device, &queue, &pipeline.texture_bind_group_layout, surface_format);
+        let default_texture = texture::Texture::create_default(
+            &device,
+            &queue,
+            &pipeline.texture_bind_group_layout,
+            texture_format,
+        );
         let text = KinoTextRenderer::new(&device, &queue, surface_format);
 
         log::debug!("Created KinoWGPURenderer");
@@ -62,6 +69,7 @@ impl KinoWGPURenderer {
             pipeline,
             default_texture,
             format: surface_format,
+            texture_format,
             size: Vec2::from_array(size),
             text,
             camera: CameraRendering {
@@ -71,6 +79,10 @@ impl KinoWGPURenderer {
         }
     }
 
+    pub fn texture_format(&self) -> wgpu::TextureFormat {
+        self.texture_format
+    }
+
     pub fn upload_camera_matrix(&mut self, queue: &wgpu::Queue, view_proj: [[f32; 4]; 4]) {
         queue.write_buffer(
             &self.camera.buffer,
diff --git a/crates/kino-ui/src/rendering/texture.rs b/crates/kino-ui/src/rendering/texture.rs
index b5e7bd7..e38ca8a 100644
--- a/crates/kino-ui/src/rendering/texture.rs
+++ b/crates/kino-ui/src/rendering/texture.rs
@@ -15,6 +15,24 @@ pub struct Texture {
 }
 
 impl Texture {
+    fn bytes_per_pixel(format: TextureFormat) -> Option<u32> {
+        match format {
+            TextureFormat::Rgba8Unorm
+            | TextureFormat::Rgba8UnormSrgb
+            | TextureFormat::Bgra8Unorm
+            | TextureFormat::Bgra8UnormSrgb => Some(4),
+            TextureFormat::Rgba16Float
+            | TextureFormat::Rgba16Unorm
+            | TextureFormat::Rgba16Snorm
+            | TextureFormat::Rgba16Uint
+            | TextureFormat::Rgba16Sint => Some(8),
+            TextureFormat::Rgba32Float
+            | TextureFormat::Rgba32Uint
+            | TextureFormat::Rgba32Sint => Some(16),
+            _ => None,
+        }
+    }
+
     /// Creates a new texture from raw RGBA image data,
     /// uploads the data, & builds the bind group using the layout
     ///
@@ -30,6 +48,19 @@ impl Texture {
         texture_format: TextureFormat,
     ) -> Self {
         log::debug!("Creating new texture");
+
+        let bytes_per_pixel = Self::bytes_per_pixel(texture_format).unwrap_or(4);
+        let expected_len = (width as usize)
+            .saturating_mul(height as usize)
+            .saturating_mul(bytes_per_pixel as usize);
+        if data.len() != expected_len {
+            log::error!(
+                "Texture data length {} does not match expected {} for {:?}",
+                data.len(),
+                expected_len,
+                texture_format
+            );
+        }
         
         let texture = device.create_texture(&TextureDescriptor {
             label: None,
@@ -56,7 +87,7 @@ impl Texture {
             data,
             TexelCopyBufferLayout {
                 offset: 0,
-                bytes_per_row: Some(4 * width),
+                bytes_per_row: Some(bytes_per_pixel * width),
                 rows_per_image: Some(height),
             },
             Extent3d {
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index c22eb9a..389d6d4 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -25,8 +25,10 @@ use eucalyptus_core::command::COMMAND_BUFFER;
 use eucalyptus_core::scene::loading::IsSceneLoaded;
 use std::collections::HashMap;
 use std::path::PathBuf;
+use log::error;
 use wgpu::SurfaceConfiguration;
 use winit::window::Fullscreen;
+use dropbear_engine::sky::{HdrLoader, SkyPipeline, DEFAULT_SKY_TEXTURE};
 // use yakui_winit::YakuiWinit;
 use dropbear_engine::texture::Texture;
 use eucalyptus_core::physics::PhysicsState;
@@ -123,6 +125,7 @@ pub struct PlayMode {
     main_pipeline: Option<MainRenderPipeline>,
     shader_globals: Option<GlobalsUniform>,
     collider_wireframe_pipeline: Option<ColliderWireframePipeline>,
+    sky_pipeline: Option<SkyPipeline>,
 
     initial_scene: Option<String>,
     current_scene: Option<String>,
@@ -207,6 +210,7 @@ impl PlayMode {
                 last_size: (0, 0),
             },
             kino: None,
+            sky_pipeline: None,
         };
 
         log::debug!("Created new play mode instance");
@@ -220,19 +224,35 @@ impl PlayMode {
         self.shader_globals = Some(GlobalsUniform::new(graphics.clone(), Some("runtime shader globals")));
         self.collider_wireframe_pipeline = Some(ColliderWireframePipeline::new(graphics.clone()));
         
-        let hdr_format = graphics.hdr.read().format();
         self.kino = Some(KinoState::new(
             KinoWGPURenderer::new(
                 &graphics.device,
                 &graphics.queue,
-                hdr_format,
+                graphics.hdr.read().format(),
                 [
                     graphics.viewport_texture.size.width as f32,
                     graphics.viewport_texture.size.height as f32,
                 ],
             ),
             KinoWinitWindowing::new(graphics.window.clone()),
-        ))
+        ));
+
+        let sky_texture = HdrLoader::from_equirectangular_bytes(
+            &graphics.device,
+            &graphics.queue,
+            DEFAULT_SKY_TEXTURE,
+            1080,
+            Some("sky texture")
+        );
+
+        match sky_texture {
+            Ok(sky_texture) => {
+                self.sky_pipeline = Some(SkyPipeline::new(graphics.clone(), sky_texture));
+            }
+            Err(e) => {
+                error!("Failed to load sky texture: {}", e);
+            }
+        }
     }
 
     fn reload_scripts_for_current_world(&mut self) {
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index 14983d5..a8989a5 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -611,13 +611,6 @@ impl Scene for PlayMode {
     }
 
     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 hdr = graphics.hdr.read();
 
         let mut encoder = CommandEncoder::new(graphics.clone(), Some("runtime viewport encoder"));
@@ -641,37 +634,34 @@ impl Scene for PlayMode {
         };
         log_once::debug_once!("Pipeline ready");
 
-        { // 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: hdr.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,
-                });
-            }
-
-            hdr.process(&mut encoder, &graphics.viewport_texture.view);
-
-            if let Err(e) = encoder.submit() {
-                log_once::error_once!("{}", e);
-            }
+        {
+            let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("viewport clear pass"),
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: hdr.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: Some(wgpu::RenderPassDepthStencilAttachment {
+                    view: &graphics.depth_texture.view,
+                    depth_ops: Some(wgpu::Operations {
+                        load: wgpu::LoadOp::Clear(0.0),
+                        store: wgpu::StoreOp::Store,
+                    }),
+                    stencil_ops: None,
+                }),
+                occlusion_query_set: None,
+                timestamp_writes: None,
+            });
         }
 
         let lights = {
@@ -763,14 +753,14 @@ impl Scene for PlayMode {
                         depth_slice: None,
                         resolve_target: None,
                         ops: wgpu::Operations {
-                            load: wgpu::LoadOp::Clear(clear_color),
+                            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::Clear(0.0),
+                            load: wgpu::LoadOp::Load,
                             store: wgpu::StoreOp::Store,
                         }),
                         stencil_ops: None,
@@ -974,13 +964,6 @@ impl Scene for PlayMode {
                 }
             }
 
-            hdr.process(&mut encoder, &graphics.viewport_texture.view);
-
-            if let Err(e) = encoder.submit() {
-                log_once::error_once!("{}", e);
-            }
-
-
             // UI_CONTEXT.with(|v| {
             //     let commands = graphics.yakui_renderer.lock().paint(
             //         &mut v.borrow().yakui_state.lock(),
@@ -998,11 +981,46 @@ impl Scene for PlayMode {
             // });
         }
 
+        if let Some(sky) = &self.sky_pipeline {
+            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                label: Some("sky render pass"),
+                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                    view: hdr.view(),
+                    resolve_target: None,
+                    ops: wgpu::Operations {
+                        load: wgpu::LoadOp::Load,
+                        store: wgpu::StoreOp::Store,
+                    },
+                    depth_slice: None,
+                })],
+                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,
+                }),
+                timestamp_writes: None,
+                occlusion_query_set: None,
+            });
+
+            render_pass.set_pipeline(&sky.pipeline);
+            render_pass.set_bind_group(0, &camera.bind_group, &[]);
+            render_pass.set_bind_group(1, &sky.bind_group, &[]);
+            render_pass.draw(0..3, 0..1);
+        }
+
+        hdr.process(&mut encoder, &graphics.viewport_texture.view);
+
+        if let Err(e) = encoder.submit() {
+            log_once::error_once!("{}", e);
+        }
+
         if let Some(kino) = &mut self.kino {
             let mut encoder = CommandEncoder::new(graphics.clone(), Some("kino encoder"));
             kino.render(&graphics.device, &graphics.queue, &mut encoder, hdr.view());
 
-            hdr.process(&mut encoder, &graphics.viewport_texture.view);
             if let Err(e) = encoder.submit() {
                 log_once::error_once!("Unable to submit kino: {}", e);
             }