use super::DEPTH_PICK_SAMPLE_SIZE;
use super::passes;
use crate::ecs::generational_registry::*;
use crate::ecs::loading::{
    loading_pipeline_is_active, loading_pipeline_mark_completed, loading_pipeline_poll_decodes,
    loading_pipeline_pop, loading_pipeline_recipe_ready, loading_pipeline_release_recipe_sources,
    loading_pipeline_requeue_front, loading_pipeline_reset_source_arena,
};
use crate::render::wgpu::rendergraph;
#[cfg(all(not(target_arch = "wasm32"), feature = "screenshot"))]
use crate::render::wgpu::rendergraph::render_graph_get_texture;
use crate::render::wgpu::rendergraph::{
    render_graph_add_pass, render_graph_compile, render_graph_execute, render_graph_get_pass_mut,
    render_graph_get_texture_view, render_graph_restore_pass_enabled_states,
    render_graph_save_pass_enabled_states, render_graph_set_external_texture,
    render_graph_set_pass_enabled,
};

fn fit_constrained_aspect(tile_width: u32, tile_height: u32, aspect: f32) -> (u32, u32) {
    let aspect = aspect.max(0.0001);
    let tile_aspect = tile_width as f32 / tile_height.max(1) as f32;
    if tile_aspect > aspect {
        let width = (tile_height as f32 * aspect).round() as u32;
        (width.max(1), tile_height.max(1))
    } else {
        let height = (tile_width as f32 / aspect).round() as u32;
        (tile_width.max(1), height.max(1))
    }
}

struct WindowDispatchContext<'a> {
    cameras: Vec<freecs::Entity>,
    target_size: (u32, u32),
    dirty_world_spheres: &'a [(nalgebra_glm::Vec3, f32)],
    frame_settings_version: u64,
    original_active_camera: Option<freecs::Entity>,
    focus_policy: crate::ecs::graphics::resources::ViewportFocusPolicy,
    mouse_position: nalgebra_glm::Vec2,
    global_dirty_signal: bool,
}

impl super::WgpuRenderer {
    pub fn render_frame(
        &mut self,
        world: &mut crate::ecs::world::World,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let _span = tracing::info_span!("render_frame").entered();
        if world.resources.renderer_state.gpu_profile.is_none() {
            world.resources.renderer_state.gpu_profile = Some(self.gpu_profile.clone());
        }
        self.frame_state.index = self.frame_state.index.wrapping_add(1);
        self.sync_present_mode(world.resources.render_settings.vsync_enabled);
        let current_settings_signature = crate::ecs::graphics::resources::render_settings_signature(
            &world.resources.render_settings,
            &world.resources.debug_draw,
            &world.resources.editor_selection,
        );
        if self.frame_state.last_settings_signature != Some(current_settings_signature) {
            world.resources.renderer_state.settings_version = world
                .resources
                .renderer_state
                .settings_version
                .wrapping_add(1);
            self.frame_state.last_settings_signature = Some(current_settings_signature);
        }

        let frame_time_ms = world.resources.window.timing.delta_time * 1000.0;
        if frame_time_ms > 0.0 {
            world
                .resources
                .renderer_state
                .adaptive_sampling
                .record_frame_time(frame_time_ms);
        }

        if self.depth_pick.pending {
            let _ = self.device.poll(wgpu::PollType::Poll);

            if self
                .depth_pick
                .map_complete
                .load(std::sync::atomic::Ordering::Relaxed)
            {
                let buffer_slice = self.depth_pick.staging_buffer.slice(..);
                let data = buffer_slice.get_mapped_range();
                let mut depth_values = Vec::new();
                let mut entity_id_values = Vec::new();
                for chunk in data.chunks_exact(8) {
                    let depth = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
                    let entity_id = u32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]);
                    depth_values.push(depth);
                    entity_id_values.push(entity_id);
                }
                drop(data);
                self.depth_pick.staging_buffer.unmap();

                world.resources.gpu_picking.set_depth_samples(
                    depth_values,
                    entity_id_values,
                    DEPTH_PICK_SAMPLE_SIZE,
                    DEPTH_PICK_SAMPLE_SIZE,
                    self.depth_pick.center.0,
                    self.depth_pick.center.1,
                );

                if let Some(camera_entity) = self.depth_pick.camera
                    && let Some(matrices) =
                        crate::ecs::camera::queries::query_camera_matrices(world, camera_entity)
                {
                    let (texture_width, texture_height) = self.depth_pick.texture_size;
                    let inverse_view_proj = (matrices.projection * matrices.view)
                        .try_inverse()
                        .unwrap_or_else(nalgebra_glm::Mat4::identity);

                    world.resources.gpu_picking.compute_result(
                        &inverse_view_proj,
                        texture_width as f32,
                        texture_height as f32,
                    );
                }

                self.depth_pick.pending = false;
                self.depth_pick
                    .map_complete
                    .store(false, std::sync::atomic::Ordering::Relaxed);
            }
        }

        #[cfg(all(not(target_arch = "wasm32"), feature = "screenshot"))]
        if self.screenshot.pending {
            let _ = self.device.poll(wgpu::PollType::Poll);

            if self
                .screenshot
                .map_complete
                .load(std::sync::atomic::Ordering::Relaxed)
            {
                let buffer_slice = self.screenshot.staging_buffer.slice(..);
                let data = buffer_slice.get_mapped_range();

                let width = self.screenshot.width;
                let height = self.screenshot.height;
                let bytes_per_pixel = 4u32;
                let unpadded_bytes_per_row = width * bytes_per_pixel;
                let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
                let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;

                let mut pixels = Vec::with_capacity((width * height * bytes_per_pixel) as usize);
                for row in 0..height {
                    let start = (row * padded_bytes_per_row) as usize;
                    let end = start + (unpadded_bytes_per_row) as usize;
                    pixels.extend_from_slice(&data[start..end]);
                }
                drop(data);
                self.screenshot.staging_buffer.unmap();

                let path = self.screenshot.path.take().unwrap_or_else(|| {
                    let screenshots_dir = std::path::PathBuf::from("screenshots");
                    if !screenshots_dir.exists() {
                        let _ = std::fs::create_dir_all(&screenshots_dir);
                    }
                    let timestamp = std::time::SystemTime::now()
                        .duration_since(std::time::UNIX_EPOCH)
                        .map(|d| d.as_secs())
                        .unwrap_or(0);
                    screenshots_dir.join(format!("screenshot_{}.png", timestamp))
                });

                let save_width = width;
                let save_height = height;
                let max_dimension = self.screenshot.max_dimension.take();
                std::thread::spawn(move || {
                    let Some(mut image) =
                        image::RgbaImage::from_raw(save_width, save_height, pixels)
                    else {
                        tracing::error!("Failed to create image from screenshot data");
                        return;
                    };
                    if let Some(limit) = max_dimension {
                        image = crop_center_square(image);
                        let (w, h) = image.dimensions();
                        let longest = w.max(h);
                        if longest > limit {
                            let scale = limit as f32 / longest as f32;
                            let new_w = ((w as f32 * scale).round() as u32).max(1);
                            let new_h = ((h as f32 * scale).round() as u32).max(1);
                            image = image::imageops::resize(
                                &image,
                                new_w,
                                new_h,
                                image::imageops::FilterType::Lanczos3,
                            );
                        }
                    }
                    if let Err(error) = image.save(&path) {
                        tracing::error!("Failed to save screenshot: {}", error);
                    } else {
                        tracing::info!("Screenshot saved to: {}", path.display());
                    }
                });

                self.screenshot.pending = false;
                self.screenshot
                    .map_complete
                    .store(false, std::sync::atomic::Ordering::Relaxed);
            }
        }

        self.resize_render_buffers(self.surface_config.width, self.surface_config.height);

        let mut ui_image_uploads = Vec::new();
        let mut texture_reload_commands = Vec::new();
        let mut hdr_skybox_commands = Vec::new();
        let mut procedural_ibl_commands = Vec::new();
        let mut ibl_snapshot_commands = Vec::new();
        #[cfg(not(target_arch = "wasm32"))]
        let mut screenshot_path: Option<Option<std::path::PathBuf>> = None;
        #[cfg(not(target_arch = "wasm32"))]
        let mut screenshot_max_dimension: Option<u32> = None;

        let render_commands = std::mem::take(&mut world.resources.commands.render);
        for cmd in render_commands {
            match cmd {
                crate::ecs::world::RenderCommand::UploadUiImageLayer {
                    layer,
                    rgba_data,
                    width,
                    height,
                } => {
                    ui_image_uploads.push((layer, rgba_data, width, height));
                }
                crate::ecs::world::RenderCommand::ReloadTexture {
                    name,
                    rgba_data,
                    width,
                    height,
                } => {
                    texture_reload_commands.push((name, rgba_data, width, height));
                }
                crate::ecs::world::RenderCommand::LoadHdrSkybox { hdr_data } => {
                    hdr_skybox_commands.push(hdr_data);
                }
                crate::ecs::world::RenderCommand::LoadHdrSkyboxFromPath { path } => {
                    match std::fs::read(&path) {
                        Ok(bytes) => {
                            tracing::info!("Successfully read HDR file: {}", path.display());
                            hdr_skybox_commands.push(bytes);
                        }
                        Err(e) => {
                            tracing::error!("Failed to read HDR file {}: {}", path.display(), e);
                        }
                    }
                }
                crate::ecs::world::RenderCommand::CaptureProceduralAtmosphereIBL {
                    atmosphere,
                    time,
                } => {
                    procedural_ibl_commands.push((atmosphere, time));
                }
                crate::ecs::world::RenderCommand::CaptureIblSnapshots { atmosphere, hours } => {
                    ibl_snapshot_commands.push((atmosphere, hours));
                }
                crate::ecs::world::RenderCommand::CaptureScreenshot {
                    path,
                    max_dimension,
                } => {
                    #[cfg(not(target_arch = "wasm32"))]
                    {
                        screenshot_path = Some(path);
                        screenshot_max_dimension = max_dimension;
                    }
                    #[cfg(target_arch = "wasm32")]
                    {
                        let _ = path;
                        let _ = max_dimension;
                    }
                }
            }
        }

        if !ui_image_uploads.is_empty() {
            for (layer, rgba_data, width, height) in &ui_image_uploads {
                self.ui_texture_array
                    .upload_layer(&self.queue, *layer, rgba_data, *width, *height);
            }
        }

        self.drain_loading_tasks(world);

        if !texture_reload_commands.is_empty() {
            use crate::render::wgpu::texture_cache::{TextureReloadResult, texture_cache_reload};

            for (name, rgba_data, width, height) in texture_reload_commands {
                let result = texture_cache_reload(
                    &mut world.resources.texture_cache,
                    &self.device,
                    &self.queue,
                    &name,
                    &rgba_data,
                    width,
                    height,
                );
                if matches!(result, TextureReloadResult::Recreated) {
                    let usage_opt =
                        registry_entry_by_name(&world.resources.texture_cache.registry, &name).map(
                            |entry| match entry.texture.format() {
                                wgpu::TextureFormat::Rgba8UnormSrgb
                                | wgpu::TextureFormat::Bgra8UnormSrgb => {
                                    crate::render::wgpu::texture_cache::TextureUsage::Color
                                }
                                _ => crate::render::wgpu::texture_cache::TextureUsage::Linear,
                            },
                        );
                    if let Some(usage) = usage_opt {
                        self.material_texture_arrays.upload(
                            &self.device,
                            &self.queue,
                            &self.mip_generator,
                            crate::render::wgpu::material_texture_arrays::MaterialTextureUpload {
                                name: name.clone(),
                                rgba_data: &rgba_data,
                                width,
                                height,
                                usage,
                                wrap_u: crate::render::wgpu::texture_cache::SamplerWrap::Repeat,
                                wrap_v: crate::render::wgpu::texture_cache::SamplerWrap::Repeat,
                            },
                        );
                    }
                    if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
                        && let Some(mesh_pass) =
                            (mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
                        && let Some(texture_entry) =
                            registry_entry_by_name(&world.resources.texture_cache.registry, &name)
                    {
                        mesh_pass.register_texture_with_data(
                            name.clone(),
                            texture_entry.view.clone(),
                            texture_entry.sampler.clone(),
                        );
                    }

                    if let Some(skinned_mesh_pass) =
                        render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
                        && let Some(skinned_mesh_pass) = (skinned_mesh_pass
                            as &mut dyn std::any::Any)
                            .downcast_mut::<passes::geometry::SkinnedMeshPass>()
                        && let Some(texture_entry) =
                            registry_entry_by_name(&world.resources.texture_cache.registry, &name)
                    {
                        skinned_mesh_pass.register_texture(
                            name.clone(),
                            texture_entry.view.clone(),
                            texture_entry.sampler.clone(),
                        );
                    }

                    if let Some(decal_pass) =
                        render_graph_get_pass_mut(&mut self.graph, "decal_pass")
                        && let Some(decal_pass) = (decal_pass as &mut dyn std::any::Any)
                            .downcast_mut::<passes::DecalPass>()
                        && let Some(texture_entry) =
                            registry_entry_by_name(&world.resources.texture_cache.registry, &name)
                    {
                        decal_pass.register_texture(
                            name,
                            texture_entry.view.clone(),
                            texture_entry.sampler.clone(),
                        );
                    }
                }
            }
        }

        #[cfg(feature = "assets")]
        if !hdr_skybox_commands.is_empty() {
            let mut ibl_views = None;

            if let Some(sky_pass) = render_graph_get_pass_mut(&mut self.graph, "sky_pass")
                && let Some(sky_pass) =
                    (sky_pass as &mut dyn std::any::Any).downcast_mut::<passes::geometry::SkyPass>()
            {
                for hdr_data in hdr_skybox_commands {
                    ibl_views =
                        Some(sky_pass.load_hdr_skybox(&self.device, &self.queue, &hdr_data));
                }
            }

            if let Some((irradiance_view, prefiltered_view)) = ibl_views {
                if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
                    && let Some(mesh_pass) =
                        (mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
                {
                    mesh_pass.update_ibl_textures(
                        self.brdf_lut_view.clone(),
                        irradiance_view.clone(),
                        prefiltered_view.clone(),
                    );
                }

                if let Some(skinned_mesh_pass) =
                    render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
                    && let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
                        .downcast_mut::<passes::geometry::SkinnedMeshPass>(
                    )
                {
                    skinned_mesh_pass.set_ibl_textures(
                        self.brdf_lut_view.clone(),
                        irradiance_view.clone(),
                        prefiltered_view.clone(),
                    );
                }

                world.resources.ibl_views.brdf_lut_view = Some(self.brdf_lut_view.clone());
                world.resources.ibl_views.irradiance_view = Some(irradiance_view);
                world.resources.ibl_views.prefiltered_view = Some(prefiltered_view);
            }
        }

        #[cfg(feature = "assets")]
        if !procedural_ibl_commands.is_empty() {
            let mut ibl_views = None;

            if let Some(sky_pass) = render_graph_get_pass_mut(&mut self.graph, "sky_pass")
                && let Some(sky_pass) =
                    (sky_pass as &mut dyn std::any::Any).downcast_mut::<passes::geometry::SkyPass>()
            {
                for (atmosphere, time) in procedural_ibl_commands {
                    ibl_views = sky_pass.capture_procedural_atmosphere(
                        &self.device,
                        &self.queue,
                        atmosphere,
                        time,
                    );
                }
            }

            if let Some((irradiance_view, prefiltered_view)) = ibl_views {
                if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
                    && let Some(mesh_pass) =
                        (mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
                {
                    mesh_pass.update_ibl_textures(
                        self.brdf_lut_view.clone(),
                        irradiance_view.clone(),
                        prefiltered_view.clone(),
                    );
                }

                if let Some(skinned_mesh_pass) =
                    render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
                    && let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
                        .downcast_mut::<passes::geometry::SkinnedMeshPass>(
                    )
                {
                    skinned_mesh_pass.set_ibl_textures(
                        self.brdf_lut_view.clone(),
                        irradiance_view.clone(),
                        prefiltered_view.clone(),
                    );
                }

                world.resources.ibl_views.brdf_lut_view = Some(self.brdf_lut_view.clone());
                world.resources.ibl_views.irradiance_view = Some(irradiance_view);
                world.resources.ibl_views.prefiltered_view = Some(prefiltered_view);
            }
        }

        #[cfg(feature = "assets")]
        if !ibl_snapshot_commands.is_empty()
            && let Some(sky_pass) = render_graph_get_pass_mut(&mut self.graph, "sky_pass")
            && let Some(sky_pass) =
                (sky_pass as &mut dyn std::any::Any).downcast_mut::<passes::geometry::SkyPass>()
        {
            for (atmosphere, hours) in ibl_snapshot_commands {
                sky_pass.capture_ibl_snapshots(&self.device, &self.queue, atmosphere, &hours);
            }
        }

        let mut frame_any_camera_wants_lines = false;
        let mut frame_any_camera_wants_wireframe = false;
        for camera in world.resources.user_interface.required_cameras.iter() {
            let shading = world
                .core
                .get_viewport_shading(*camera)
                .copied()
                .unwrap_or_default();
            let post_process = world.core.get_camera_post_process(*camera).copied();
            let culling_mask = world.core.get_camera_culling_mask(*camera).copied();
            let environment = world.core.get_camera_environment(*camera).copied();
            let effective = crate::ecs::camera::components::EffectiveShading::for_camera(
                &world.resources.render_settings,
                &world.resources.debug_draw,
                &shading,
                post_process.as_ref(),
                culling_mask.as_ref(),
                environment.as_ref(),
            );
            if effective.show_normals || effective.show_bounding_volumes {
                frame_any_camera_wants_lines = true;
            }
            if effective.show_wireframe {
                frame_any_camera_wants_wireframe = true;
            }
        }
        let upload_lines = world.resources.debug_draw.show_normals
            || world.resources.debug_draw.show_bounding_volumes
            || frame_any_camera_wants_lines;

        if let Some(lines_pass) = render_graph_get_pass_mut(&mut self.graph, "lines_pass")
            && let Some(lines_pass) =
                (lines_pass as &mut dyn std::any::Any).downcast_mut::<passes::LinesPass>()
        {
            passes::sync_lines_data(
                lines_pass,
                &self.device,
                &self.queue,
                world,
                frame_any_camera_wants_wireframe,
            );
            passes::sync_bounding_volume_data(
                lines_pass,
                &self.device,
                &self.queue,
                world,
                upload_lines,
                world.resources.debug_draw.show_selected_bounding_volume,
                world
                    .resources
                    .editor_selection
                    .bounding_volume_selected_entity,
            );
            passes::sync_normal_data(
                lines_pass,
                &self.device,
                &self.queue,
                world,
                upload_lines,
                world.resources.debug_draw.normal_line_length,
                world.resources.debug_draw.normal_line_color,
            );
        }

        let point_shadow_view = render_graph_get_pass_mut(&mut self.graph, "shadow_depth_pass")
            .and_then(|pass| {
                (pass as &dyn std::any::Any)
                    .downcast_ref::<passes::ShadowDepthPass>()
                    .map(|shadow_pass| shadow_pass.point_light_cubemap_view().clone())
            });

        if let Some(view) = point_shadow_view {
            if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
                && let Some(mesh_pass) =
                    (mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
            {
                mesh_pass.update_point_shadow_cubemap(view.clone());
            }
            if let Some(skinned_mesh_pass) =
                render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
                && let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
                    .downcast_mut::<passes::SkinnedMeshPass>()
            {
                skinned_mesh_pass.update_point_shadow_cubemap(view);
            }
        }

        {
            let raw_hour = world.resources.renderer_state.day_night.hour;
            let hour = raw_hour - (raw_hour / 24.0).floor() * 24.0;
            let world_id = world.resources.world_id;
            let mut bracket_views: Option<(
                wgpu::TextureView,
                wgpu::TextureView,
                wgpu::TextureView,
                wgpu::TextureView,
                f32,
            )> = None;

            if let Some(sky_pass) = render_graph_get_pass_mut(&mut self.graph, "sky_pass")
                && let Some(sky_pass) =
                    (sky_pass as &mut dyn std::any::Any).downcast_mut::<passes::geometry::SkyPass>()
                && let Some((index_a, index_b, blend)) =
                    sky_pass.select_ibl_bracket(world.resources.render_settings.atmosphere, hour)
            {
                let irr_a =
                    sky_pass.ibl_snapshots[index_a]
                        .1
                        .create_view(&wgpu::TextureViewDescriptor {
                            dimension: Some(wgpu::TextureViewDimension::Cube),
                            ..Default::default()
                        });
                let pref_a =
                    sky_pass.ibl_snapshots[index_a]
                        .2
                        .create_view(&wgpu::TextureViewDescriptor {
                            dimension: Some(wgpu::TextureViewDimension::Cube),
                            ..Default::default()
                        });
                let irr_b =
                    sky_pass.ibl_snapshots[index_b]
                        .1
                        .create_view(&wgpu::TextureViewDescriptor {
                            dimension: Some(wgpu::TextureViewDimension::Cube),
                            ..Default::default()
                        });
                let pref_b =
                    sky_pass.ibl_snapshots[index_b]
                        .2
                        .create_view(&wgpu::TextureViewDescriptor {
                            dimension: Some(wgpu::TextureViewDimension::Cube),
                            ..Default::default()
                        });
                bracket_views = Some((irr_a, pref_a, irr_b, pref_b, blend));
            }

            if let Some((irr_a, pref_a, irr_b, pref_b, blend)) = bracket_views {
                world.resources.render_settings.ibl_blend_factor = blend;

                if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
                    && let Some(mesh_pass) =
                        (mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
                {
                    mesh_pass.update_ibl_textures_blended(
                        self.brdf_lut_view.clone(),
                        irr_a.clone(),
                        pref_a.clone(),
                        irr_b.clone(),
                        pref_b.clone(),
                        blend,
                    );
                }

                if let Some(skinned_mesh_pass) =
                    render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
                    && let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
                        .downcast_mut::<passes::geometry::SkinnedMeshPass>(
                    )
                {
                    skinned_mesh_pass.update_ibl_textures_blended_for_world(
                        world_id,
                        passes::geometry::BlendedIblViews {
                            brdf_lut: self.brdf_lut_view.clone(),
                            irradiance_a: irr_a.clone(),
                            prefiltered_a: pref_a.clone(),
                            irradiance_b: irr_b,
                            prefiltered_b: pref_b,
                            blend_factor: blend,
                        },
                    );
                }

                world.resources.ibl_views.brdf_lut_view = Some(self.brdf_lut_view.clone());
                world.resources.ibl_views.irradiance_view = Some(irr_a);
                world.resources.ibl_views.prefiltered_view = Some(pref_a);
            }
        }

        let surface_texture = match self.surface.get_current_texture() {
            wgpu::CurrentSurfaceTexture::Success(frame)
            | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
            wgpu::CurrentSurfaceTexture::Outdated => {
                self.surface.configure(&self.device, &self.surface_config);
                match self.surface.get_current_texture() {
                    wgpu::CurrentSurfaceTexture::Success(frame)
                    | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame,
                    other => {
                        panic!("Failed to get surface texture after reconfiguration: {other:?}")
                    }
                }
            }
            _ => {
                return Ok(());
            }
        };

        let surface_texture_view =
            surface_texture
                .texture
                .create_view(&wgpu::TextureViewDescriptor {
                    label: wgpu::Label::default(),
                    aspect: wgpu::TextureAspect::default(),
                    format: Some(self.surface_format),
                    dimension: None,
                    base_mip_level: 0,
                    mip_level_count: None,
                    base_array_layer: 0,
                    array_layer_count: None,
                    usage: None,
                });

        render_graph_set_external_texture(
            &mut self.graph,
            self.targets.swapchain,
            Some(surface_texture.texture.clone()),
            surface_texture_view,
            self.surface_config.width,
            self.surface_config.height,
        );

        render_graph_set_external_texture(
            &mut self.graph,
            self.targets.spotlight_shadow_atlas,
            Some(self.spotlight_shadow_atlas_texture.clone()),
            self.spotlight_shadow_atlas_view.clone(),
            self.spotlight_shadow_atlas_texture.width(),
            self.spotlight_shadow_atlas_texture.height(),
        );

        let active_camera_entities: std::collections::HashSet<freecs::Entity> = world
            .resources
            .user_interface
            .required_cameras
            .iter()
            .copied()
            .collect();
        self.cleanup_unused_camera_viewports(&active_camera_entities);

        world
            .resources
            .user_interface
            .viewport_texture_sizes
            .clear();

        let dirty_world_spheres: Vec<(nalgebra_glm::Vec3, f32)> = {
            let mut spheres = Vec::new();
            let dirty_entities: Vec<_> = world
                .resources
                .mesh_render_state
                .dirty_entities_for_culling()
                .collect();
            for entity in dirty_entities {
                let Some(global_transform) = world.core.get_global_transform(entity) else {
                    continue;
                };
                let Some(bounding_volume) = world.core.get_bounding_volume(entity) else {
                    continue;
                };
                let world_center = global_transform.0
                    * nalgebra_glm::Vec4::new(
                        bounding_volume.obb.center.x,
                        bounding_volume.obb.center.y,
                        bounding_volume.obb.center.z,
                        1.0,
                    );
                let world_center =
                    nalgebra_glm::Vec3::new(world_center.x, world_center.y, world_center.z);
                let scale = nalgebra_glm::length(&nalgebra_glm::Vec3::new(
                    global_transform.0[(0, 0)],
                    global_transform.0[(1, 0)],
                    global_transform.0[(2, 0)],
                ));
                let world_radius = bounding_volume.sphere_radius * scale;
                spheres.push((world_center, world_radius));
            }
            spheres
        };

        let global_dirty_signal = world
            .resources
            .mesh_render_state
            .requires_full_invalidation();
        let frame_settings_version = world.resources.renderer_state.settings_version;

        let mouse_position = world.resources.input.mouse.position;
        let original_active_camera = world.resources.active_camera;
        let focus_policy = world.resources.renderer_state.focus_policy;

        let primary_cameras = world.resources.user_interface.required_cameras.clone();
        self.dispatch_window_renders(
            world,
            WindowDispatchContext {
                cameras: primary_cameras,
                target_size: (self.surface_config.width, self.surface_config.height),
                dirty_world_spheres: &dirty_world_spheres,
                frame_settings_version,
                original_active_camera,
                focus_policy,
                mouse_position,
                global_dirty_signal,
            },
        )?;

        #[cfg(not(target_arch = "wasm32"))]
        {
            if !self.screenshot.pending
                && let Some(path) = screenshot_path
                && let Some(compute_output_texture) =
                    render_graph_get_texture(&self.graph, self.targets.compute_output)
            {
                let width = self.surface_config.width;
                let height = self.surface_config.height;
                let bytes_per_pixel = 4u32;
                let unpadded_bytes_per_row = width * bytes_per_pixel;
                let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
                let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;

                let required_buffer_size = (padded_bytes_per_row * height) as u64;
                if self.screenshot.staging_buffer.size() < required_buffer_size {
                    self.screenshot.staging_buffer =
                        self.device.create_buffer(&wgpu::BufferDescriptor {
                            label: Some("Screenshot Staging Buffer"),
                            size: required_buffer_size,
                            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
                            mapped_at_creation: false,
                        });
                }

                let mut encoder =
                    self.device
                        .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                            label: Some("Screenshot Encoder"),
                        });

                encoder.copy_texture_to_buffer(
                    wgpu::TexelCopyTextureInfo {
                        texture: compute_output_texture,
                        mip_level: 0,
                        origin: wgpu::Origin3d::ZERO,
                        aspect: wgpu::TextureAspect::All,
                    },
                    wgpu::TexelCopyBufferInfo {
                        buffer: &self.screenshot.staging_buffer,
                        layout: wgpu::TexelCopyBufferLayout {
                            offset: 0,
                            bytes_per_row: Some(padded_bytes_per_row),
                            rows_per_image: Some(height),
                        },
                    },
                    wgpu::Extent3d {
                        width,
                        height,
                        depth_or_array_layers: 1,
                    },
                );

                self.queue.submit(std::iter::once(encoder.finish()));

                self.screenshot.path = path;
                self.screenshot.width = width;
                self.screenshot.height = height;
                self.screenshot.max_dimension = screenshot_max_dimension;
                self.screenshot.pending = true;

                let map_complete = self.screenshot.map_complete.clone();
                self.screenshot.staging_buffer.slice(..).map_async(
                    wgpu::MapMode::Read,
                    move |_| {
                        map_complete.store(true, std::sync::atomic::Ordering::Relaxed);
                    },
                );
            }
        }

        surface_texture.present();

        Ok(())
    }

    fn sync_present_mode(&mut self, vsync_enabled: bool) {
        let desired =
            super::initialization::pick_present_mode(&self.supported_present_modes, vsync_enabled);
        if desired == self.surface_config.present_mode {
            return;
        }
        self.surface_config.present_mode = desired;
        self.surface.configure(&self.device, &self.surface_config);
    }

    pub fn resize_surface(
        &mut self,
        width: u32,
        height: u32,
    ) -> Result<(), Box<dyn std::error::Error>> {
        if width == 0 || height == 0 {
            return Ok(());
        }

        self.surface_config.width = width;
        self.surface_config.height = height;
        self.surface.configure(&self.device, &self.surface_config);
        self.resize_render_buffers(width, height);

        Ok(())
    }

    pub fn configure_with_state(
        &mut self,
        state: &mut dyn crate::run::State,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let mut text_pass = passes::TextPass::new(
            &self.device,
            wgpu::TextureFormat::Rgba16Float,
            wgpu::TextureFormat::Depth32Float,
        );
        text_pass.update_glyph_atlas(&self.device, &self.glyph_atlas.texture_view);

        render_graph_add_pass(
            &mut self.graph,
            Box::new(text_pass),
            &[
                ("color", self.targets.scene_color),
                ("depth", self.targets.depth),
            ],
        )?;

        state.configure_render_graph(
            &mut self.graph,
            &self.device,
            self.surface_format,
            crate::run::RenderResources {
                scene_color: self.targets.scene_color,
                depth: self.targets.depth,
                compute_output: self.targets.compute_output,
                swapchain: self.targets.swapchain,
                view_normals: self.targets.view_normals,
                ssao_raw: self.targets.ssao_raw,
                ssao: self.targets.ssao,
                ssgi_raw: self.targets.ssgi_raw,
                ssgi: self.targets.ssgi,
                ssr_raw: self.targets.ssr_raw,
                ssr: self.targets.ssr,
                surface_width: self.surface_config.width,
                surface_height: self.surface_config.height,
            },
        );

        let viewport_blit_pass = passes::BlitPass::new(&self.device, self.surface_format)
            .with_name("viewport_blit_pass");
        render_graph_add_pass(
            &mut self.graph,
            Box::new(viewport_blit_pass),
            &[
                ("input", self.targets.fxaa_output),
                ("output", self.targets.viewport_resource),
            ],
        )?;

        let cached_viewport_preload_pass = passes::BlitPass::new(&self.device, self.surface_format)
            .with_name("cached_viewport_preload_pass")
            .with_runs_in_full(false)
            .with_runs_in_compose_only(true);
        render_graph_add_pass(
            &mut self.graph,
            Box::new(cached_viewport_preload_pass),
            &[
                ("input", self.targets.viewport_resource),
                ("output", self.targets.fxaa_output),
            ],
        )?;

        let swapchain_compose_pass =
            passes::ViewportComposePass::new(&self.device, self.surface_format);
        render_graph_add_pass(
            &mut self.graph,
            Box::new(swapchain_compose_pass),
            &[
                ("input", self.targets.fxaa_output),
                ("output", self.targets.swapchain),
            ],
        )?;

        let mut ui_pass = passes::UiPass::new(&self.device, self.surface_format);
        ui_pass.update_glyph_atlas(&self.device, &self.glyph_atlas.texture_view);
        render_graph_add_pass(
            &mut self.graph,
            Box::new(ui_pass),
            &[
                ("color", self.targets.swapchain),
                ("depth", self.targets.ui_depth),
            ],
        )?;

        if let Some(ui_image_pass) = self.ui_image_pass.take() {
            render_graph_add_pass(
                &mut self.graph,
                ui_image_pass,
                &[
                    ("color", self.targets.swapchain),
                    ("depth", self.targets.ui_depth),
                ],
            )?;
        }

        let pick_keepalive_pass = passes::PickKeepalivePass::new();
        render_graph_add_pass(
            &mut self.graph,
            Box::new(pick_keepalive_pass),
            &[
                ("entity_id", self.targets.entity_id),
                ("depth", self.targets.depth),
            ],
        )?;

        render_graph_compile(&mut self.graph)?;
        Ok(())
    }

    pub fn initialize_fonts(&mut self, _world: &mut crate::ecs::world::World) {
        self.frame_state.glyph_atlas_initialized = true;
    }

    pub fn update_with_state(
        &mut self,
        state: &mut dyn crate::run::State,
        world: &mut crate::ecs::world::World,
    ) -> Result<(), Box<dyn std::error::Error>> {
        self.initialize_fonts(world);

        for pending_load in world.resources.loading.pending_font_loads.drain(..) {
            world
                .resources
                .text
                .font_engine
                .load_font(pending_load.font_data);
        }

        state.update_render_graph(&mut self.graph, world);
        Ok(())
    }

    pub fn register_render_texture(&mut self, name: &str, view: wgpu::TextureView) {
        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
            label: Some(&format!("{}_sampler", name)),
            address_mode_u: wgpu::AddressMode::ClampToEdge,
            address_mode_v: wgpu::AddressMode::ClampToEdge,
            address_mode_w: wgpu::AddressMode::ClampToEdge,
            mag_filter: wgpu::FilterMode::Linear,
            min_filter: wgpu::FilterMode::Linear,
            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
            ..Default::default()
        });

        if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
            && let Some(mesh_pass) =
                (mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
        {
            mesh_pass.register_texture(name.to_string(), view, sampler);
        }
    }
}

struct MaterialTextureUploadRequest<'a> {
    texture: crate::ecs::asset_id::TextureId,
    rgba_data: &'a [u8],
    width: u32,
    height: u32,
    usage: crate::render::wgpu::texture_cache::TextureUsage,
    sampler: crate::render::wgpu::texture_cache::SamplerSettings,
}

impl super::WgpuRenderer {
    fn dispatch_pick_compute_if_pending(&mut self, world: &mut crate::ecs::world::World) {
        if self.depth_pick.pending {
            return;
        }
        let Some(request) = world.resources.gpu_picking.take_pending_request() else {
            return;
        };
        let Some(depth_texture_view) =
            render_graph_get_texture_view(&self.graph, self.targets.depth)
        else {
            return;
        };
        let Some(entity_id_texture_view) =
            render_graph_get_texture_view(&self.graph, self.targets.entity_id)
        else {
            return;
        };

        let uniform_data: [u32; 4] = [
            request.screen_x,
            request.screen_y,
            DEPTH_PICK_SAMPLE_SIZE,
            0,
        ];
        self.queue.write_buffer(
            &self.depth_pick.uniform_buffer,
            0,
            bytemuck::cast_slice(&uniform_data),
        );

        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
            label: Some("Depth Pick Bind Group"),
            layout: &self.depth_pick.bind_group_layout,
            entries: &[
                wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::TextureView(depth_texture_view),
                },
                wgpu::BindGroupEntry {
                    binding: 1,
                    resource: self.depth_pick.storage_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 2,
                    resource: self.depth_pick.uniform_buffer.as_entire_binding(),
                },
                wgpu::BindGroupEntry {
                    binding: 3,
                    resource: wgpu::BindingResource::TextureView(entity_id_texture_view),
                },
            ],
        });

        let mut encoder = self
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("Depth Pick Encoder"),
            });

        {
            let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
                label: Some("Depth Pick Pass"),
                timestamp_writes: None,
            });

            compute_pass.set_pipeline(&self.depth_pick.compute_pipeline);
            compute_pass.set_bind_group(0, &bind_group, &[]);
            compute_pass.dispatch_workgroups(DEPTH_PICK_SAMPLE_SIZE, DEPTH_PICK_SAMPLE_SIZE, 1);
        }

        encoder.copy_buffer_to_buffer(
            &self.depth_pick.storage_buffer,
            0,
            &self.depth_pick.staging_buffer,
            0,
            (DEPTH_PICK_SAMPLE_SIZE * DEPTH_PICK_SAMPLE_SIZE * 8) as u64,
        );

        self.queue.submit(std::iter::once(encoder.finish()));
        self.depth_pick.bind_group = Some(bind_group);
        self.depth_pick.pending = true;
        self.depth_pick.center = (request.screen_x, request.screen_y);
        self.depth_pick.texture_size = self.render_buffer_size;
        self.depth_pick.camera = world.resources.active_camera;

        let map_complete = self.depth_pick.map_complete.clone();
        self.depth_pick
            .staging_buffer
            .slice(..)
            .map_async(wgpu::MapMode::Read, move |_| {
                map_complete.store(true, std::sync::atomic::Ordering::Relaxed);
            });
    }

    fn clear_swapchain_for_window_ui(&mut self, world: &crate::ecs::world::World) {
        let Some(view) =
            rendergraph::render_graph_get_texture_view(&self.graph, self.targets.swapchain)
        else {
            return;
        };
        let clear = world
            .resources
            .retained_ui
            .background_color
            .map(|color| wgpu::Color {
                r: color.x as f64,
                g: color.y as f64,
                b: color.z as f64,
                a: color.w as f64,
            })
            .unwrap_or(wgpu::Color {
                r: 0.04,
                g: 0.04,
                b: 0.06,
                a: 1.0,
            });
        let mut encoder = self
            .device
            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("Window UI Swapchain Clear"),
            });
        {
            let _pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                label: Some("Window UI Swapchain Clear Pass"),
                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                    view,
                    resolve_target: None,
                    ops: wgpu::Operations {
                        load: wgpu::LoadOp::Clear(clear),
                        store: wgpu::StoreOp::Store,
                    },
                    depth_slice: None,
                })],
                depth_stencil_attachment: None,
                timestamp_writes: None,
                occlusion_query_set: None,
                multiview_mask: None,
            });
        }
        self.queue.submit(std::iter::once(encoder.finish()));
    }

    fn dispatch_window_renders(
        &mut self,
        world: &mut crate::ecs::world::World,
        ctx: WindowDispatchContext<'_>,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let _span =
            tracing::info_span!("dispatch_window_renders", cameras = ctx.cameras.len(),).entered();
        let WindowDispatchContext {
            cameras,
            target_size,
            dirty_world_spheres,
            frame_settings_version,
            original_active_camera,
            focus_policy,
            mouse_position,
            global_dirty_signal,
        } = ctx;

        let (surface_width, surface_height) = target_size;
        let has_viewports = !cameras.is_empty();
        let _ = render_graph_set_pass_enabled(&mut self.graph, "viewport_blit_pass", has_viewports);
        let _ = render_graph_set_pass_enabled(&mut self.graph, "viewport_compose_pass", true);

        if !has_viewports {
            self.resize_render_buffers_for_window(surface_width, surface_height);
        }

        world.resources.window.camera_tile_render_iteration = 0;

        let ui_pass_states_before_loop = if has_viewports {
            let saved_states = render_graph_save_pass_enabled_states(&self.graph);
            let saved = (
                saved_states.get("ui_pass").copied().unwrap_or(true),
                saved_states.get("ui_image_pass").copied().unwrap_or(true),
            );
            let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_pass", false);
            let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_image_pass", false);
            Some(saved)
        } else {
            None
        };

        let mouse_focused_camera = world
            .resources
            .window
            .camera_tile_rects
            .iter()
            .find(|(_, rect)| rect.contains(mouse_position))
            .map(|(entity, _)| *entity);
        let focused_camera = mouse_focused_camera.or(original_active_camera);

        let requested_anisotropy = world
            .resources
            .render_settings
            .material_anisotropy_filtering;
        let anisotropy_changed = self
            .material_texture_arrays
            .set_anisotropy(&self.device, requested_anisotropy);
        if self.material_texture_arrays.take_bindless_dirty() || anisotropy_changed {
            if let Some(pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
                && let Some(mesh_pass) =
                    (pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
            {
                mesh_pass.apply_material_textures(&self.device, &self.material_texture_arrays);
            }
            if let Some(pass) = render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
                && let Some(skinned) = (pass as &mut dyn std::any::Any)
                    .downcast_mut::<passes::geometry::SkinnedMeshPass>()
            {
                skinned.apply_material_textures(&self.device, &self.material_texture_arrays);
            }
        }

        for (iteration_index, camera_entity) in cameras.iter().enumerate() {
            let (tile_width, tile_height) = world
                .resources
                .window
                .camera_tile_rects
                .get(camera_entity)
                .map(|rect| {
                    (
                        (rect.width.round() as u32).max(1),
                        (rect.height.round() as u32).max(1),
                    )
                })
                .unwrap_or((surface_width.max(1), surface_height.max(1)));
            let constrained = world.core.get_constrained_aspect(*camera_entity).copied();
            let (display_width, display_height) = if let Some(aspect) = constrained {
                fit_constrained_aspect(tile_width, tile_height, aspect.0)
            } else {
                (tile_width, tile_height)
            };
            let render_scale = world
                .resources
                .render_settings
                .render_scale
                .clamp(0.25, 4.0);
            let is_main_viewport_camera = Some(*camera_entity) == original_active_camera;
            let render_width = ((display_width as f32 * render_scale).round() as u32).max(1);
            let render_height = ((display_height as f32 * render_scale).round() as u32).max(1);

            self.ensure_camera_viewport(*camera_entity, render_width, render_height);
            self.resize_render_buffers_for_window(render_width, render_height);

            let viewport_cloned = self.camera_viewports.get(camera_entity).map(|viewport| {
                (
                    viewport.texture.clone(),
                    viewport.view.clone(),
                    viewport.size,
                    viewport.has_rendered_at_least_once,
                    viewport.last_active_view,
                    viewport.last_settings_version,
                    viewport.last_camera_world_transform,
                )
            });

            if let Some((
                texture,
                view,
                size,
                has_rendered_once,
                last_active_view,
                last_settings_version,
                last_camera_world_transform,
            )) = viewport_cloned
            {
                render_graph_set_external_texture(
                    &mut self.graph,
                    self.targets.viewport_resource,
                    Some(texture),
                    view,
                    render_width,
                    render_height,
                );

                world.resources.active_camera = Some(*camera_entity);
                world.resources.window.camera_tile_render_iteration = iteration_index as u32;

                let viewport_shading = world
                    .core
                    .get_viewport_shading(*camera_entity)
                    .copied()
                    .unwrap_or_default();
                let update_mode = world
                    .core
                    .get_viewport_update_mode(*camera_entity)
                    .copied()
                    .unwrap_or_default();

                let camera_post_process =
                    world.core.get_camera_post_process(*camera_entity).copied();
                let camera_culling_mask =
                    world.core.get_camera_culling_mask(*camera_entity).copied();
                let camera_environment = world.core.get_camera_environment(*camera_entity).copied();
                let effective = crate::ecs::camera::components::EffectiveShading::for_camera(
                    &world.resources.render_settings,
                    &world.resources.debug_draw,
                    &viewport_shading,
                    camera_post_process.as_ref(),
                    camera_culling_mask.as_ref(),
                    camera_environment.as_ref(),
                );

                let is_focused_view = Some(*camera_entity) == focused_camera;
                let focus_forces_render = matches!(
                    focus_policy,
                    crate::ecs::graphics::resources::ViewportFocusPolicy::FocusedAlways
                ) && is_focused_view;
                let view_changed = last_active_view != Some(effective);
                let settings_changed = last_settings_version != frame_settings_version;
                let camera_matrices =
                    crate::ecs::camera::queries::query_camera_matrices(world, *camera_entity);
                let camera_world_transform = world
                    .core
                    .get_global_transform(*camera_entity)
                    .map(|gt| gt.0);
                let camera_self_dirty = match (last_camera_world_transform, camera_world_transform)
                {
                    (Some(prev), Some(curr)) => prev != curr,
                    (None, _) => true,
                    (_, None) => false,
                };
                let frustum_dirty = if let Some(matrices) = camera_matrices.as_ref() {
                    let view_proj = matrices.projection * matrices.view;
                    let frustum_planes = passes::geometry::extract_frustum_planes(&view_proj);
                    dirty_world_spheres.iter().any(|(center, radius)| {
                        passes::geometry::sphere_in_frustum(center, *radius, &frustum_planes)
                    })
                } else {
                    !dirty_world_spheres.is_empty()
                };
                let pick_forces_render =
                    is_main_viewport_camera && world.resources.gpu_picking.has_pending_request();
                let camera_force_render = world
                    .resources
                    .window
                    .force_render_cameras
                    .contains(camera_entity);
                let should_render = pick_forces_render
                    || camera_force_render
                    || match update_mode {
                        crate::ecs::camera::components::ViewportUpdateMode::Always => true,
                        crate::ecs::camera::components::ViewportUpdateMode::WhenVisible => true,
                        crate::ecs::camera::components::ViewportUpdateMode::WhenDirty => {
                            focus_forces_render
                                || !has_rendered_once
                                || global_dirty_signal
                                || settings_changed
                                || view_changed
                                || camera_self_dirty
                                || frustum_dirty
                        }
                        crate::ecs::camera::components::ViewportUpdateMode::Once => {
                            !has_rendered_once
                        }
                        crate::ecs::camera::components::ViewportUpdateMode::Disabled => {
                            !has_rendered_once
                        }
                    };

                world.resources.renderer_state.active_view = effective;

                if self.frame_state.extract_dirty_done_for_frame != self.frame_state.index {
                    let _span = tracing::info_span!("extract_frame_dirty_state").entered();
                    self.extract_frame_dirty_state(world);
                    self.frame_state.extract_dirty_done_for_frame = self.frame_state.index;
                }
                if self.frame_state.text_meshes_prepared_for_frame != self.frame_state.index {
                    let _span = tracing::info_span!("prepare_text_meshes").entered();
                    self.prepare_text_meshes(world);
                    self.frame_state.text_meshes_prepared_for_frame = self.frame_state.index;
                }
                let phase = if should_render {
                    rendergraph::ExecutePhase::Full
                } else {
                    rendergraph::ExecutePhase::ComposeOnly
                };
                let command_buffers = {
                    let _span =
                        tracing::info_span!("render_graph_execute", phase = ?phase).entered();
                    rendergraph::render_graph_execute_with_phase(
                        &mut self.graph,
                        &self.device,
                        &self.queue,
                        world,
                        phase,
                    )?
                };
                {
                    let _span = tracing::info_span!("queue_submit").entered();
                    self.queue.submit(command_buffers);
                }

                if should_render
                    && let Some(viewport) = self.camera_viewports.get_mut(camera_entity)
                {
                    viewport.has_rendered_at_least_once = true;
                    viewport.last_active_view = Some(effective);
                    viewport.last_settings_version = frame_settings_version;
                    viewport.last_render_frame = self.frame_state.index;
                    viewport.last_camera_world_transform = camera_world_transform;
                }
                if should_render {
                    world
                        .resources
                        .window
                        .force_render_cameras
                        .remove(camera_entity);
                }

                if is_main_viewport_camera {
                    self.dispatch_pick_compute_if_pending(world);
                }

                world
                    .resources
                    .user_interface
                    .viewport_texture_sizes
                    .push(size);
            }
        }

        world.resources.active_camera = original_active_camera;
        world.resources.renderer_state.active_view =
            crate::ecs::camera::components::EffectiveShading::from_graphics(
                &world.resources.render_settings,
                &world.resources.debug_draw,
            );

        if let Some((ui_was_enabled, ui_image_was_enabled)) = ui_pass_states_before_loop {
            let _span = tracing::info_span!("ui_composite").entered();

            let _ = rendergraph::render_graph_resize_transient_resource(
                &mut self.graph,
                &self.device,
                self.targets.ui_depth,
                surface_width.max(1),
                surface_height.max(1),
            );

            let swapped_ui = swap_window_ui_frame(world, target_size);

            let saved_pass_states = render_graph_save_pass_enabled_states(&self.graph);
            for name in saved_pass_states.keys() {
                let _ = render_graph_set_pass_enabled(&mut self.graph, name, false);
            }
            let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_pass", ui_was_enabled);
            let _ = render_graph_set_pass_enabled(
                &mut self.graph,
                "ui_image_pass",
                ui_image_was_enabled,
            );
            self.extract_frame_dirty_state(world);
            self.prepare_text_meshes(world);
            let command_buffers = {
                let _span = tracing::info_span!("ui_composite_execute").entered();
                render_graph_execute(&mut self.graph, &self.device, &self.queue, world)?
            };
            self.queue.submit(command_buffers);

            restore_window_ui_frame(world, swapped_ui);

            render_graph_restore_pass_enabled_states(&mut self.graph, &saved_pass_states);
            let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_pass", ui_was_enabled);
            let _ = render_graph_set_pass_enabled(
                &mut self.graph,
                "ui_image_pass",
                ui_image_was_enabled,
            );
        }

        if cameras.is_empty() {
            world.resources.window.camera_tile_render_iteration = 0;

            if world.resources.render_settings.render_world_to_swapchain {
                self.extract_frame_dirty_state(world);
                self.prepare_text_meshes(world);
                let command_buffers =
                    render_graph_execute(&mut self.graph, &self.device, &self.queue, world)?;
                self.queue.submit(command_buffers);
                self.dispatch_pick_compute_if_pending(world);
            } else {
                let swapped_ui = swap_window_ui_frame(world, target_size);
                self.clear_swapchain_for_window_ui(world);

                let _ = rendergraph::render_graph_resize_transient_resource(
                    &mut self.graph,
                    &self.device,
                    self.targets.ui_depth,
                    surface_width.max(1),
                    surface_height.max(1),
                );

                let saved_pass_states = render_graph_save_pass_enabled_states(&self.graph);
                for name in saved_pass_states.keys() {
                    let _ = render_graph_set_pass_enabled(&mut self.graph, name, false);
                }
                let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_pass", true);
                let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_image_pass", true);

                self.extract_frame_dirty_state(world);
                self.prepare_text_meshes(world);
                let command_buffers =
                    render_graph_execute(&mut self.graph, &self.device, &self.queue, world)?;
                self.queue.submit(command_buffers);

                restore_window_ui_frame(world, swapped_ui);

                render_graph_restore_pass_enabled_states(&mut self.graph, &saved_pass_states);
                let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_pass", true);
                let _ = render_graph_set_pass_enabled(&mut self.graph, "ui_image_pass", true);
            }
        }

        let dispatched_settings_version = world.resources.renderer_state.settings_version;
        self.record_window_dispatch(dispatched_settings_version);
        Ok(())
    }

    fn upload_material_texture(
        &mut self,
        world: &mut crate::ecs::world::World,
        request: MaterialTextureUploadRequest<'_>,
    ) {
        let MaterialTextureUploadRequest {
            texture,
            rgba_data,
            width,
            height,
            usage,
            sampler,
        } = request;
        let Some(name) = registry_name_for(&world.resources.texture_cache.registry, texture.index)
            .map(str::to_string)
        else {
            tracing::error!(
                "upload_material_texture: no name registered for texture id {}",
                texture.index
            );
            return;
        };
        if let Err(e) =
            crate::render::wgpu::texture_cache::texture_cache_load_from_raw_rgba_with_format(
                &mut world.resources.texture_cache,
                &self.device,
                &self.queue,
                &self.mip_generator,
                crate::render::wgpu::texture_cache::TextureUploadRequest {
                    name: name.clone(),
                    rgba_data,
                    dimensions: (width, height),
                    spec: crate::render::wgpu::texture_cache::TextureUploadSpec {
                        format: usage.wgpu_format(),
                        sampler,
                    },
                },
            )
        {
            tracing::error!("Failed to load texture: {}", e);
            return;
        }

        let layer_index = self.material_texture_arrays.upload(
            &self.device,
            &self.queue,
            &self.mip_generator,
            crate::render::wgpu::material_texture_arrays::MaterialTextureUpload {
                name: name.clone(),
                rgba_data,
                width,
                height,
                usage,
                wrap_u: sampler.wrap_u,
                wrap_v: sampler.wrap_v,
            },
        );

        if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
            && let Some(mesh_pass) =
                (mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
            && let Some(texture_entry) =
                registry_entry_by_name(&world.resources.texture_cache.registry, &name)
        {
            mesh_pass.register_texture_with_data(
                name.clone(),
                texture_entry.view.clone(),
                texture_entry.sampler.clone(),
            );
            if let Some(layer) = layer_index {
                mesh_pass.add_material_layer_mapping(
                    texture,
                    crate::render::wgpu::material_texture_arrays::MaterialTextureLayer {
                        usage,
                        layer,
                        wrap_u: sampler.wrap_u,
                        wrap_v: sampler.wrap_v,
                    },
                );
            }
        }

        if let Some(skinned_mesh_pass) =
            render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
            && let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
                .downcast_mut::<passes::geometry::SkinnedMeshPass>()
            && let Some(texture_entry) =
                registry_entry_by_name(&world.resources.texture_cache.registry, &name)
        {
            skinned_mesh_pass.register_texture(
                name.clone(),
                texture_entry.view.clone(),
                texture_entry.sampler.clone(),
            );
            if let Some(layer) = layer_index {
                skinned_mesh_pass.add_material_layer_mapping(
                    texture,
                    crate::render::wgpu::material_texture_arrays::MaterialTextureLayer {
                        usage,
                        layer,
                        wrap_u: sampler.wrap_u,
                        wrap_v: sampler.wrap_v,
                    },
                );
            }
        }

        if let Some(decal_pass) = render_graph_get_pass_mut(&mut self.graph, "decal_pass")
            && let Some(decal_pass) =
                (decal_pass as &mut dyn std::any::Any).downcast_mut::<passes::DecalPass>()
            && let Some(texture_entry) =
                registry_entry_by_name(&world.resources.texture_cache.registry, &name)
        {
            decal_pass.register_texture(
                name,
                texture_entry.view.clone(),
                texture_entry.sampler.clone(),
            );
        }
    }

    fn reserve_material_layer(
        &mut self,
        world: &mut crate::ecs::world::World,
        texture: crate::ecs::asset_id::TextureId,
        usage: crate::render::wgpu::texture_cache::TextureUsage,
        sampler: crate::render::wgpu::texture_cache::SamplerSettings,
    ) -> bool {
        let Some(name) = registry_name_for(&world.resources.texture_cache.registry, texture.index)
            .map(str::to_string)
        else {
            return false;
        };
        let Some(layer) = self.material_texture_arrays.reserve_layer(
            &self.queue,
            name,
            usage,
            sampler.wrap_u,
            sampler.wrap_v,
        ) else {
            return false;
        };
        let mapping = crate::render::wgpu::material_texture_arrays::MaterialTextureLayer {
            usage,
            layer,
            wrap_u: sampler.wrap_u,
            wrap_v: sampler.wrap_v,
        };
        if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
            && let Some(mesh_pass) =
                (mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
        {
            mesh_pass.add_material_layer_mapping(texture, mapping);
        }
        if let Some(skinned_mesh_pass) =
            render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
            && let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
                .downcast_mut::<passes::geometry::SkinnedMeshPass>()
        {
            skinned_mesh_pass.add_material_layer_mapping(texture, mapping);
        }
        true
    }

    pub(super) fn drain_loading_tasks(&mut self, world: &mut crate::ecs::world::World) {
        // Collect images the worker pool finished decoding. The GPU upload below
        // is the only throttled step; decoding already ran off this thread.
        loading_pipeline_poll_decodes(
            &mut world.resources.loading.pipeline,
            crate::ecs::loading::DECODE_POLL_BUDGET,
        );

        let budget = world.resources.loading.pipeline.tasks_per_frame.max(1);
        let mut uploaded_textures: Vec<crate::ecs::asset_id::TextureId> = Vec::new();
        let mut deferred: Vec<crate::ecs::loading::LoadingTask> = Vec::new();
        let mut reserved_any = false;
        let mut uploads = 0usize;
        while uploads < budget {
            let Some(task) = loading_pipeline_pop(&mut world.resources.loading.pipeline) else {
                break;
            };
            let category = task.category();
            let label = match &task {
                crate::ecs::loading::LoadingTask::ReserveLayer { texture, .. }
                | crate::ecs::loading::LoadingTask::UploadDecodedTexture { texture, .. }
                | crate::ecs::loading::LoadingTask::MaterializeTexture { texture, .. } => {
                    registry_name_for(&world.resources.texture_cache.registry, texture.index)
                        .map(str::to_string)
                        .unwrap_or_else(|| format!("texture {}", texture.index))
                }
            };
            match task {
                crate::ecs::loading::LoadingTask::ReserveLayer {
                    texture,
                    usage,
                    sampler,
                } => {
                    // Reserving a stable layer and writing its placeholder is cheap and
                    // must happen before geometry first renders, so it does not draw from
                    // the per-frame upload budget.
                    if self.reserve_material_layer(world, texture, usage, sampler) {
                        reserved_any = true;
                    }
                }
                crate::ecs::loading::LoadingTask::UploadDecodedTexture {
                    texture,
                    rgba_data,
                    width,
                    height,
                    usage,
                    sampler,
                } => {
                    self.upload_material_texture(
                        world,
                        MaterialTextureUploadRequest {
                            texture,
                            rgba_data: &rgba_data,
                            width,
                            height,
                            usage,
                            sampler,
                        },
                    );
                    uploaded_textures.push(texture);
                    uploads += 1;
                }
                crate::ecs::loading::LoadingTask::MaterializeTexture {
                    texture,
                    recipe,
                    usage,
                    sampler,
                } => {
                    // Hold the task until every source image it samples has finished
                    // decoding in the worker pool, then upload into the reserved layer.
                    if !loading_pipeline_recipe_ready(&world.resources.loading.pipeline, &recipe) {
                        deferred.push(crate::ecs::loading::LoadingTask::MaterializeTexture {
                            texture,
                            recipe,
                            usage,
                            sampler,
                        });
                        continue;
                    }
                    let produced = crate::ecs::loading::execute_texture_recipe(
                        &recipe,
                        &world.resources.loading.pipeline.decoded_images,
                    );
                    loading_pipeline_release_recipe_sources(
                        &mut world.resources.loading.pipeline,
                        &recipe,
                    );
                    if let Some(decoded) = produced {
                        // Capture the materialized RGBA into texture_sources for any
                        // texture whose recipe is composite (RG-pack, RGB+A pack,
                        // spec/gloss conversion). Direct-recipe textures already have
                        // their PNG bytes captured at queue_gltf_load and are skipped
                        // here via the `entry`-style insert.
                        let texture_name = world
                            .resources
                            .texture_cache
                            .registry
                            .index_to_name
                            .get(texture.index as usize)
                            .and_then(|slot| slot.clone());
                        if let Some(name) = texture_name {
                            world
                                .resources
                                .assets
                                .texture_sources
                                .entry(name)
                                .or_insert_with(|| crate::ecs::asset_state::TextureSourceBytes {
                                    data: crate::ecs::asset_state::TextureSourceData::Rgba {
                                        rgba: decoded.rgba.clone(),
                                        width: decoded.width,
                                        height: decoded.height,
                                    },
                                    usage,
                                    sampler,
                                });
                        }
                        self.upload_material_texture(
                            world,
                            MaterialTextureUploadRequest {
                                texture,
                                rgba_data: &decoded.rgba,
                                width: decoded.width,
                                height: decoded.height,
                                usage,
                                sampler,
                            },
                        );
                        uploaded_textures.push(texture);
                    } else {
                        tracing::warn!("texture recipe produced no data for '{}'", label);
                    }
                    uploads += 1;
                }
            }
            loading_pipeline_mark_completed(&mut world.resources.loading.pipeline, label, category);
        }
        for task in deferred.into_iter().rev() {
            loading_pipeline_requeue_front(&mut world.resources.loading.pipeline, task);
        }
        // Stable layers mean a streamed pixel upload never changes a material's layer
        // index, so only a fresh reservation (or a freshly inserted material) needs the
        // resolve plus instance rebuild.
        let needs_resolve = reserved_any
            || !world
                .resources
                .assets
                .material_registry
                .pending_resolve
                .is_empty();
        if needs_resolve {
            crate::ecs::material::resources::material_registry_resolve_uploaded_textures(
                &mut world.resources.assets.material_registry,
                &world.resources.texture_cache,
                &uploaded_textures,
            );
            world.resources.mesh_render_state.request_full_rebuild();
        }
        if !loading_pipeline_is_active(&world.resources.loading.pipeline) {
            loading_pipeline_reset_source_arena(&mut world.resources.loading.pipeline);
        }

        let evicted = crate::render::wgpu::texture_cache::texture_cache_remove_unused(
            &mut world.resources.texture_cache,
        );
        if !evicted.is_empty() {
            for name in &evicted {
                self.material_texture_arrays.release(name);
                world.resources.assets.texture_sources.remove(name);
            }
            if let Some(mesh_pass) = render_graph_get_pass_mut(&mut self.graph, "mesh_pass")
                && let Some(mesh_pass) =
                    (mesh_pass as &mut dyn std::any::Any).downcast_mut::<passes::MeshPass>()
            {
                for name in &evicted {
                    mesh_pass.unregister_texture(name);
                }
            }
            if let Some(skinned_mesh_pass) =
                render_graph_get_pass_mut(&mut self.graph, "skinned_mesh_pass")
                && let Some(skinned_mesh_pass) = (skinned_mesh_pass as &mut dyn std::any::Any)
                    .downcast_mut::<passes::geometry::SkinnedMeshPass>(
                )
            {
                for name in &evicted {
                    skinned_mesh_pass.unregister_texture(name);
                }
            }
            if let Some(decal_pass) = render_graph_get_pass_mut(&mut self.graph, "decal_pass")
                && let Some(decal_pass) =
                    (decal_pass as &mut dyn std::any::Any).downcast_mut::<passes::DecalPass>()
            {
                for name in &evicted {
                    decal_pass.unregister_texture(name);
                }
            }
            world.resources.mesh_render_state.request_full_rebuild();
        }
    }
}

struct SwappedWindowUi;

fn swap_window_ui_frame(
    _world: &mut crate::ecs::world::World,
    _target_size: (u32, u32),
) -> SwappedWindowUi {
    SwappedWindowUi
}

fn restore_window_ui_frame(_world: &mut crate::ecs::world::World, _saved: SwappedWindowUi) {}

#[cfg(not(target_arch = "wasm32"))]
fn crop_center_square(image: image::RgbaImage) -> image::RgbaImage {
    let (width, height) = image.dimensions();
    if width == height {
        return image;
    }
    let side = width.min(height);
    let x = (width - side) / 2;
    let y = (height - side) / 2;
    image::imageops::crop_imm(&image, x, y, side, side).to_image()
}
