//! Bounded reconstructed viewport rendering and capture ownership.

#![allow(clippy::missing_errors_doc)]
#![allow(
    clippy::cast_possible_truncation,
    clippy::cast_possible_wrap,
    clippy::cast_precision_loss,
    clippy::cast_sign_loss
)]

use base64::Engine as _;
use libremetaverse_types::{
    UUID,
    compat::{CancellationToken, CancellationTokenSource},
};
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::fmt::{self, Write as _};
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::sync::Semaphore;

pub use metacrate_rendering_wgpu::{
    Camera as CameraPose, Renderable as SceneRenderable, Texture as SceneTexture,
};

#[derive(Clone, Debug, PartialEq)]
pub struct SceneTriangle {
    pub vertices: [[f32; 3]; 3],
    pub colors_srgb: [[u8; 4]; 3],
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SceneEntityKind {
    Object,
    Resident,
    Terrain,
    Environment,
}

#[derive(Clone, Debug, PartialEq)]
pub struct SceneEntity {
    pub id: UUID,
    pub kind: SceneEntityKind,
    pub display_name: String,
    pub triangles: Vec<SceneTriangle>,
    pub renderables: Vec<SceneRenderable>,
    pub texture_available: bool,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize)]
pub struct SnapshotCompleteness {
    pub objects_truncated: bool,
    pub avatars_truncated: bool,
    pub textures_missing: u32,
    pub terrain_available: bool,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct SceneBuildTimings {
    pub terrain_wait: Duration,
    pub object_snapshot_and_asset_discovery: Duration,
    pub material_fetch: Duration,
    pub asset_fetch: Duration,
    pub decode_and_geometry: Duration,
}

#[derive(Clone, Debug, PartialEq)]
pub struct SceneSnapshot {
    pub generation: u64,
    /// Changes only when the persistent render scene changes; camera movement does not change it.
    pub scene_sequence: u64,
    pub observed_unix_millis: u64,
    pub region_id: UUID,
    pub region_name: String,
    pub camera: CameraPose,
    pub entities: Arc<[SceneEntity]>,
    pub textures: Arc<[SceneTexture]>,
    pub completeness: SnapshotCompleteness,
    /// Work already performed by the scene source; validated again before rendering.
    pub texture_fetches: usize,
    pub texture_bytes: usize,
    pub decoded_texture_pixels: usize,
    pub timings: SceneBuildTimings,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct VisionLimits {
    pub width: u32,
    pub height: u32,
    pub max_distance_meters: u32,
    pub asset_cache_max_bytes: u64,
    pub max_entities: usize,
    pub max_triangles: usize,
    pub max_texture_fetches: usize,
    pub max_texture_bytes: usize,
    pub max_decode_pixels: usize,
    pub max_jpeg_bytes: usize,
    pub max_concurrent_captures: usize,
    pub capture_timeout: Duration,
    pub minimum_interval: Duration,
}

impl Default for VisionLimits {
    fn default() -> Self {
        Self {
            width: 1920,
            height: 1080,
            max_distance_meters: 64,
            asset_cache_max_bytes: 2 * 1024 * 1024 * 1024,
            max_entities: 2_048,
            max_triangles: 1_048_576,
            max_texture_fetches: 1_024,
            max_texture_bytes: 512 * 1024 * 1024,
            max_decode_pixels: 268_435_456,
            max_jpeg_bytes: 2 * 1024 * 1024,
            max_concurrent_captures: 1,
            capture_timeout: Duration::from_mins(8),
            minimum_interval: Duration::from_secs(5),
        }
    }
}
impl VisionLimits {
    pub(crate) fn valid(self) -> bool {
        (64..=1_920).contains(&self.width)
            && (64..=1_080).contains(&self.height)
            && self.width as usize * self.height as usize <= 2_073_600
            && (1..=1_024).contains(&self.max_distance_meters)
            && (64 * 1024 * 1024..=64 * 1024 * 1024 * 1024).contains(&self.asset_cache_max_bytes)
            && (1..=4_096).contains(&self.max_entities)
            && (1..=1_048_576).contains(&self.max_triangles)
            && self.max_texture_fetches <= 1_024
            && self.max_texture_bytes <= 512 * 1024 * 1024
            && self.max_decode_pixels <= 536_870_912
            && (4_096..=4 * 1024 * 1024).contains(&self.max_jpeg_bytes)
            && (1..=4).contains(&self.max_concurrent_captures)
            && !self.capture_timeout.is_zero()
            && self.capture_timeout <= Duration::from_mins(10)
            && self.minimum_interval <= Duration::from_mins(10)
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum VisionError {
    UnsafeLimits,
    NotExplicit,
    Unauthorized,
    RateLimited,
    Busy,
    Cancelled,
    Superseded,
    TimedOut,
    StaleGeneration,
    InvalidScene,
    ResourceLimit,
    Encode,
    EndpointUnsupported,
}
impl fmt::Display for VisionError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Self::UnsafeLimits => "unsafe viewport limits",
            Self::NotExplicit => "message does not explicitly request visual context",
            Self::Unauthorized => "visual capture origin is not authorized",
            Self::RateLimited => "visual capture rate limit reached",
            Self::Busy => "visual capture capacity is exhausted",
            Self::Cancelled => "visual capture was cancelled",
            Self::Superseded => "visual capture was superseded",
            Self::TimedOut => "visual capture timed out",
            Self::StaleGeneration => "visual capture belongs to a stale simulator generation",
            Self::InvalidScene => "scene data is invalid",
            Self::ResourceLimit => "visual capture exceeded a resource limit",
            Self::Encode => "viewport image encoding failed",
            Self::EndpointUnsupported => "configured endpoint rejected multimodal input",
        })
    }
}
impl std::error::Error for VisionError {}

pub type VisionFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T, VisionError>> + Send + 'a>>;
pub trait SceneSource: Send + Sync + 'static {
    fn capture_scene(
        &self,
        generation: u64,
        cancellation: CancellationToken,
    ) -> VisionFuture<'_, SceneSnapshot>;
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum VisionObservationKind {
    Started,
    SceneCaptured,
    Rendered,
    Completed,
    Cancelled,
    Failed,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct VisionObservation {
    pub correlation_id: String,
    pub generation: u64,
    pub kind: VisionObservationKind,
    pub image_sha256: Option<String>,
    pub detail: String,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct VisionCapture {
    pub jpeg: Vec<u8>,
    pub data_url: String,
    pub image_sha256: String,
    pub summary: String,
    pub width: u32,
    pub height: u32,
    pub generation: u64,
    pub observed_unix_millis: u64,
    pub completeness: SnapshotCompleteness,
}

struct VisionState {
    next_allowed: Instant,
    active: Option<(String, CancellationTokenSource)>,
    observations: Vec<VisionObservation>,
}
pub struct VisionService<S: SceneSource> {
    source: Arc<S>,
    limits: VisionLimits,
    semaphore: Semaphore,
    request_sequence: AtomicU64,
    current_generation: AtomicU64,
    state: Mutex<VisionState>,
    #[cfg(feature = "live-grid")]
    gpu_preferred: bool,
    #[cfg(feature = "live-grid")]
    gpu: tokio::sync::OnceCell<Option<Arc<metacrate_game_loop::ViewportLoop>>>,
    #[cfg(feature = "live-grid")]
    viewport_task: Mutex<Option<tokio::task::JoinHandle<()>>>,
}

impl<S: SceneSource> VisionService<S> {
    pub fn new(source: Arc<S>, limits: VisionLimits) -> Result<Self, VisionError> {
        if !limits.valid() {
            return Err(VisionError::UnsafeLimits);
        }
        Ok(Self {
            source,
            limits,
            semaphore: Semaphore::new(limits.max_concurrent_captures),
            request_sequence: AtomicU64::new(0),
            current_generation: AtomicU64::new(0),
            state: Mutex::new(VisionState {
                next_allowed: Instant::now(),
                active: None,
                observations: Vec::new(),
            }),
            #[cfg(feature = "live-grid")]
            gpu_preferred: false,
            #[cfg(feature = "live-grid")]
            gpu: tokio::sync::OnceCell::new(),
            #[cfg(feature = "live-grid")]
            viewport_task: Mutex::new(None),
        })
    }
    /// Prefer cached offscreen `wgpu` rendering, falling back to the
    /// deterministic software rasterizer when no adapter is available.
    #[cfg(feature = "live-grid")]
    #[must_use]
    pub fn prefer_gpu(mut self) -> Self {
        self.gpu_preferred = true;
        self
    }

    /// Completes renderer discovery before the live agent is allowed to become active.
    #[cfg(feature = "live-grid")]
    pub async fn initialize_renderer(&self) -> bool {
        !self.gpu_preferred || self.renderer().await.is_some()
    }

    #[cfg(feature = "live-grid")]
    async fn renderer(&self) -> Option<Arc<metacrate_game_loop::ViewportLoop>> {
        self.gpu
            .get_or_init(|| async {
                tokio::task::spawn_blocking(bevy_renderer_new)
                    .await
                    .ok()
                    .and_then(Result::ok)
                    .map(Arc::new)
            })
            .await
            .clone()
    }

    /// Returns the last fully completed viewport without waiting for or
    /// interfering with the next render.
    #[cfg(feature = "live-grid")]
    #[must_use]
    pub fn latest_viewport(
        &self,
    ) -> Option<metacrate_game_loop::Published<metacrate_game_loop::ViewportFrame>> {
        self.gpu.get()?.as_ref()?.latest()
    }

    #[cfg(feature = "live-grid")]
    #[must_use]
    pub fn viewport_stats(&self) -> Option<metacrate_game_loop::ViewportStats> {
        Some(self.gpu.get()?.as_ref()?.stats())
    }

    /// Starts the scene feeder. The viewport crate owns the fixed-rate render
    /// loop and completed-frame publication after submission.
    #[cfg(feature = "live-grid")]
    pub fn start_viewport(self: &Arc<Self>, generation: u64, cancellation: CancellationToken) {
        if let Some(task) = lock(&self.viewport_task).take() {
            task.abort();
        }
        let service = Arc::downgrade(self);
        *lock(&self.viewport_task) = Some(tokio::spawn(async move {
            loop {
                if cancellation.is_cancellation_requested() {
                    break;
                }
                let Some(service) = service.upgrade() else {
                    break;
                };
                if let Ok(scene) = service
                    .source
                    .capture_scene(generation, cancellation.clone())
                    .await
                {
                    service.submit_viewport_scene(&scene).await;
                }
                drop(service);
                tokio::select! {
                    () = cancellation.cancelled() => break,
                    () = tokio::time::sleep(Duration::from_millis(100)) => {}
                }
            }
        }));
    }

    #[cfg(feature = "live-grid")]
    pub async fn stop_viewport(&self) {
        let task = lock(&self.viewport_task).take();
        if let Some(task) = task {
            task.abort();
            let _ = task.await;
        }
    }

    #[cfg(feature = "live-grid")]
    pub async fn wait_viewport_ready(&self, timeout: Duration) -> bool {
        tokio::time::timeout(timeout, async {
            loop {
                if self.latest_viewport().is_some() {
                    return;
                }
                tokio::time::sleep(Duration::from_millis(20)).await;
            }
        })
        .await
        .is_ok()
    }

    #[cfg(feature = "live-grid")]
    async fn submit_viewport_scene(&self, scene: &SceneSnapshot) {
        let Some(viewport) = self.renderer().await else {
            return;
        };
        viewport.submit(viewport_scene(scene, self.limits));
    }
    pub fn set_generation(&self, generation: u64) {
        if self.current_generation.swap(generation, Ordering::AcqRel) != generation {
            self.cancel_active();
        }
    }
    #[must_use]
    pub fn generation(&self) -> u64 {
        self.current_generation.load(Ordering::Acquire)
    }
    pub fn cancel_active(&self) -> bool {
        lock(&self.state).active.take().is_some_and(|(_, source)| {
            source.cancel();
            true
        })
    }
    #[must_use]
    pub fn observations(&self) -> Vec<VisionObservation> {
        lock(&self.state).observations.clone()
    }
    fn observe(
        &self,
        correlation: &str,
        generation: u64,
        kind: VisionObservationKind,
        hash: Option<String>,
        detail: &str,
    ) {
        let mut state = lock(&self.state);
        if state.observations.len() == 256 {
            state.observations.remove(0);
        }
        state.observations.push(VisionObservation {
            correlation_id: correlation.to_owned(),
            generation,
            kind,
            image_sha256: hash,
            detail: detail.to_owned(),
        });
    }
    pub async fn capture(
        &self,
        correlation: &str,
        generation: u64,
        cancellation: CancellationToken,
    ) -> Result<VisionCapture, VisionError> {
        if generation == 0 || generation != self.current_generation.load(Ordering::Acquire) {
            return Err(VisionError::StaleGeneration);
        }
        let owned = CancellationTokenSource::new();
        {
            let mut state = lock(&self.state);
            if Instant::now() < state.next_allowed {
                return Err(VisionError::RateLimited);
            }
            if let Some((_, previous)) = state.active.take() {
                previous.cancel();
            }
            state.next_allowed = Instant::now() + self.limits.minimum_interval;
        }
        let _permit = tokio::select! {
            () = cancellation.cancelled() => return Err(VisionError::Cancelled),
            permit = self.semaphore.acquire() => permit.map_err(|_| VisionError::Cancelled)?,
        };
        lock(&self.state).active = Some((correlation.to_owned(), owned.clone()));
        let request = self.request_sequence.fetch_add(1, Ordering::AcqRel) + 1;
        self.observe(
            correlation,
            generation,
            VisionObservationKind::Started,
            None,
            "viewport capture started",
        );
        let token = owned.token();
        let result = tokio::select! {()=cancellation.cancelled()=>Err(VisionError::Cancelled),()=token.cancelled()=>Err(VisionError::Superseded),result=tokio::time::timeout(self.limits.capture_timeout,self.source.capture_scene(generation,token.clone()))=>result.map_err(|_|VisionError::TimedOut)?};
        let result = if result == Err(VisionError::Cancelled)
            && token.is_cancellation_requested()
            && !cancellation.is_cancellation_requested()
        {
            Err(VisionError::Superseded)
        } else {
            result
        };
        let result = match result {
            Ok(scene) => {
                self.observe(
                    correlation,
                    generation,
                    VisionObservationKind::SceneCaptured,
                    None,
                    "bounded scene captured",
                );
                if request != self.request_sequence.load(Ordering::Acquire) {
                    Err(VisionError::Superseded)
                } else if generation != self.current_generation.load(Ordering::Acquire) {
                    Err(VisionError::StaleGeneration)
                } else {
                    self.render_scene(scene).await.inspect(|capture| {
                        self.observe(
                            correlation,
                            generation,
                            VisionObservationKind::Rendered,
                            Some(capture.image_sha256.clone()),
                            "deterministic viewport rendered",
                        );
                    })
                }
            }
            Err(error) => Err(error),
        };
        lock(&self.state).active = None;
        match &result {
            Ok(capture) => self.observe(
                correlation,
                generation,
                VisionObservationKind::Completed,
                Some(capture.image_sha256.clone()),
                "capture completed",
            ),
            Err(VisionError::Cancelled | VisionError::Superseded) => self.observe(
                correlation,
                generation,
                VisionObservationKind::Cancelled,
                None,
                "capture cancelled",
            ),
            Err(_) => self.observe(
                correlation,
                generation,
                VisionObservationKind::Failed,
                None,
                "capture failed",
            ),
        }
        result
    }

    async fn render_scene(&self, scene: SceneSnapshot) -> Result<VisionCapture, VisionError> {
        #[cfg(feature = "live-grid")]
        if self.gpu_preferred
            && let Some(renderer) = self.renderer().await
        {
            renderer.submit(viewport_scene(&scene, self.limits));
            let deadline = Instant::now() + self.limits.capture_timeout;
            while Instant::now() < deadline {
                if let Some(frame) = renderer.latest()
                    && frame.value.scene_sequence == scene.scene_sequence
                    && frame.value.camera == scene.camera
                {
                    return finish_capture(&scene, self.limits, &frame.value.rgba);
                }
                tokio::time::sleep(Duration::from_millis(5)).await;
            }
        }
        render_scene_software(&scene, self.limits)
    }
}

#[cfg(feature = "live-grid")]
fn bevy_renderer_new()
-> Result<metacrate_game_loop::ViewportLoop, metacrate_rendering_wgpu::RenderError> {
    metacrate_game_loop::ViewportLoop::start(metacrate_game_loop::DEFAULT_VIEWPORT_FRAME_INTERVAL)
}

#[cfg(feature = "live-grid")]
fn viewport_scene(
    scene: &SceneSnapshot,
    limits: VisionLimits,
) -> metacrate_game_loop::ViewportScene {
    let mut renderables = scene
        .entities
        .iter()
        .flat_map(|entity| {
            entity
                .renderables
                .iter()
                .cloned()
                .map(move |renderable| (entity.id.to_string(), renderable))
        })
        .collect::<Vec<_>>();
    renderables.sort_by(|left, right| left.0.cmp(&right.0));
    metacrate_game_loop::ViewportScene {
        sequence: scene.scene_sequence,
        camera: scene.camera,
        renderables: renderables
            .into_iter()
            .map(|(_, renderable)| renderable)
            .collect::<Vec<_>>()
            .into(),
        textures: Arc::clone(&scene.textures),
        background_srgb: environment_color(scene),
        limits: metacrate_rendering_wgpu::RenderLimits {
            width: limits.width,
            height: limits.height,
            max_triangles: limits.max_triangles,
            max_texture_bytes: limits.max_texture_bytes,
            max_texture_pixels: limits.max_decode_pixels,
            far_distance: 512,
        },
    }
}

/// Type-erased runtime control surface. It deliberately exposes metadata and
/// hashes, never captured image bytes.
pub trait VisionControl: Send + Sync {
    fn cancel_capture(&self, correlation_id: &str) -> bool;
    fn active_capture(&self) -> Option<String>;
    fn capture_progress(&self) -> Option<String>;
    fn last_image_sha256(&self) -> Option<String>;
}

impl<S: SceneSource> VisionControl for VisionService<S> {
    fn cancel_capture(&self, correlation_id: &str) -> bool {
        let mut state = lock(&self.state);
        if state
            .active
            .as_ref()
            .is_some_and(|(active, _)| active == correlation_id)
        {
            if let Some((_, source)) = state.active.take() {
                source.cancel();
            }
            true
        } else {
            false
        }
    }

    fn active_capture(&self) -> Option<String> {
        lock(&self.state)
            .active
            .as_ref()
            .map(|(correlation, _)| correlation.clone())
    }

    fn capture_progress(&self) -> Option<String> {
        let state = lock(&self.state);
        let active = state.active.as_ref()?.0.as_str();
        state
            .observations
            .iter()
            .rev()
            .find(|event| event.correlation_id == active)
            .map(|event| event.detail.clone())
    }

    fn last_image_sha256(&self) -> Option<String> {
        lock(&self.state)
            .observations
            .iter()
            .rev()
            .find_map(|event| event.image_sha256.clone())
    }
}

/// Provider-neutral responder decorator that adds a reconstructed viewport only
/// for an explicit visual question. An endpoint capability rejection produces
/// a textual scene-summary response without retrying the large image request.
pub struct VisionAugmentedResponder<S: SceneSource, R: crate::interaction::InteractionResponder> {
    vision: Arc<VisionService<S>>,
    inner: Arc<R>,
}

impl<S: SceneSource, R: crate::interaction::InteractionResponder> VisionAugmentedResponder<S, R> {
    #[must_use]
    pub fn new(vision: Arc<VisionService<S>>, inner: Arc<R>) -> Self {
        Self { vision, inner }
    }
}

impl<S: SceneSource, R: crate::interaction::InteractionResponder>
    crate::interaction::InteractionResponder for VisionAugmentedResponder<S, R>
{
    fn respond(
        &self,
        mut request: crate::interaction::ResponseRequest,
        cancellation: CancellationToken,
    ) -> crate::interaction::ResponderFuture<'_> {
        Box::pin(async move {
            let explicit = request
                .messages
                .iter()
                .rev()
                .find_map(|message| {
                    message
                        .content
                        .as_slice()
                        .iter()
                        .find_map(|part| match part {
                            crate::llm::ContentPart::Text(text) => {
                                Some(visual_question(text.as_str()))
                            }
                            crate::llm::ContentPart::Image { .. } => None,
                        })
                })
                .unwrap_or(false);
            if !explicit {
                return self.inner.respond(request, cancellation).await;
            }
            if request.origin != crate::interaction::InteractionOrigin::AuthorizedIm {
                return Err(crate::interaction::InteractionModelError::Failed);
            }
            let capture = self
                .vision
                .capture(
                    request.delivery_id.as_str(),
                    self.vision.generation(),
                    cancellation.clone(),
                )
                .await
                .map_err(|error| match error {
                    VisionError::Cancelled | VisionError::Superseded => {
                        crate::interaction::InteractionModelError::Cancelled
                    }
                    VisionError::TimedOut => crate::interaction::InteractionModelError::Timeout,
                    _ => crate::interaction::InteractionModelError::Failed,
                })?;
            let target = request
                .messages
                .iter_mut()
                .rev()
                .find(|message| message.role == crate::types::MessageRole::Avatar)
                .ok_or(crate::interaction::InteractionModelError::Failed)?;
            target
                .content
                .try_push(
                    "vision.summary",
                    crate::llm::ContentPart::Text(
                        crate::types::BoundedText::new("vision.summary", capture.summary.clone())
                            .map_err(|_| crate::interaction::InteractionModelError::Failed)?,
                    ),
                )
                .map_err(|_| crate::interaction::InteractionModelError::Failed)?;
            target
                .content
                .try_push(
                    "vision.image",
                    crate::llm::ContentPart::Image {
                        url: crate::types::BoundedText::new("vision.image", capture.data_url)
                            .map_err(|_| crate::interaction::InteractionModelError::Failed)?,
                        detail: crate::llm::ImageDetail::Low,
                    },
                )
                .map_err(|_| crate::interaction::InteractionModelError::Failed)?;
            match self.inner.respond(request, cancellation).await {
                Err(crate::interaction::InteractionModelError::MultimodalUnsupported) => {
                    crate::interaction::VisibleResponse::new(format!(
                        "The configured endpoint does not support visual input. Scene summary: {}",
                        capture.summary
                    ))
                    .map_err(|_| crate::interaction::InteractionModelError::Failed)
                }
                result => result,
            }
        })
    }
}

#[must_use]
pub fn visual_question(text: &str) -> bool {
    let lower = text.to_lowercase();
    [
        "what do you see",
        "what can you see",
        "look at",
        "in front of you",
        "visual",
        "viewport",
        "picture",
        "image",
        "screenshot",
    ]
    .iter()
    .any(|needle| lower.contains(needle))
}

fn render_scene_software(
    scene: &SceneSnapshot,
    limits: VisionLimits,
) -> Result<VisionCapture, VisionError> {
    validate_scene(scene, limits)?;
    let pixel_count = limits.width as usize * limits.height as usize;
    let mut rgba = vec![0u8; pixel_count * 4];
    let sky = environment_color(scene);
    for pixel in rgba.chunks_exact_mut(4) {
        pixel.copy_from_slice(&sky);
    }
    let mut depth = vec![f32::INFINITY; pixel_count];
    let basis = camera_basis(scene.camera)?;
    let mut triangles = scene
        .entities
        .iter()
        .flat_map(|entity| {
            entity
                .triangles
                .iter()
                .map(move |triangle| (entity, triangle))
        })
        .collect::<Vec<_>>();
    triangles.sort_by_key(|(entity, _)| entity.id.to_string());
    for (_, triangle) in triangles {
        raster_triangle(
            &mut rgba,
            &mut depth,
            limits.width,
            limits.height,
            scene.camera,
            basis,
            triangle,
        );
    }
    finish_capture(scene, limits, &rgba)
}

fn finish_capture(
    scene: &SceneSnapshot,
    limits: VisionLimits,
    rgba: &[u8],
) -> Result<VisionCapture, VisionError> {
    let jpeg = encode_jpeg(limits.width, limits.height, rgba, limits.max_jpeg_bytes)?;
    let hash = Sha256::digest(&jpeg)
        .iter()
        .fold(String::with_capacity(64), |mut output, byte| {
            write!(output, "{byte:02x}").expect("writing to String cannot fail");
            output
        });
    let summary = scene_summary(scene);
    let data_url = format!(
        "data:image/jpeg;base64,{}",
        base64::engine::general_purpose::STANDARD.encode(&jpeg)
    );
    Ok(VisionCapture {
        jpeg,
        data_url,
        image_sha256: hash,
        summary,
        width: limits.width,
        height: limits.height,
        generation: scene.generation,
        observed_unix_millis: scene.observed_unix_millis,
        completeness: scene.completeness,
    })
}
fn validate_scene(scene: &SceneSnapshot, limits: VisionLimits) -> Result<(), VisionError> {
    if scene.generation == 0
        || scene.region_id == UUID::zero()
        || scene.region_name.len() > 256
        || scene.entities.len() > limits.max_entities
    {
        return Err(VisionError::InvalidScene);
    }
    if scene.texture_fetches > limits.max_texture_fetches
        || scene.texture_bytes > limits.max_texture_bytes
        || scene.decoded_texture_pixels > limits.max_decode_pixels
    {
        return Err(VisionError::ResourceLimit);
    }
    let mut count = 0usize;
    for entity in scene.entities.iter() {
        count = count
            .checked_add(entity.triangles.len())
            .ok_or(VisionError::ResourceLimit)?;
        if entity.display_name.len() > 256
            || entity
                .triangles
                .iter()
                .flat_map(|t| t.vertices)
                .flatten()
                .any(|v| !v.is_finite())
        {
            return Err(VisionError::InvalidScene);
        }
    }
    if count > limits.max_triangles {
        return Err(VisionError::ResourceLimit);
    }
    Ok(())
}
type CameraBasis = ([f32; 3], [f32; 3], [f32; 3]);
fn camera_basis(camera: CameraPose) -> Result<CameraBasis, VisionError> {
    if !(10.0..=140.0).contains(&camera.vertical_fov_degrees)
        || camera
            .position
            .iter()
            .chain(camera.forward.iter())
            .chain(camera.up.iter())
            .any(|v| !v.is_finite())
    {
        return Err(VisionError::InvalidScene);
    }
    let f = normalize(camera.forward)?;
    let r = normalize(cross(f, camera.up))?;
    let u = cross(r, f);
    Ok((r, u, f))
}
fn normalize(v: [f32; 3]) -> Result<[f32; 3], VisionError> {
    let length = dot(v, v).sqrt();
    if length < 0.0001 {
        return Err(VisionError::InvalidScene);
    }
    Ok([v[0] / length, v[1] / length, v[2] / length])
}
fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
    [
        a[1] * b[2] - a[2] * b[1],
        a[2] * b[0] - a[0] * b[2],
        a[0] * b[1] - a[1] * b[0],
    ]
}
fn raster_triangle(
    rgba: &mut [u8],
    depth: &mut [f32],
    width: u32,
    height: u32,
    camera: CameraPose,
    basis: ([f32; 3], [f32; 3], [f32; 3]),
    triangle: &SceneTriangle,
) {
    let mut p = [[0f32; 3]; 3];
    let aspect = width as f32 / height as f32;
    let focal = 1.0 / (camera.vertical_fov_degrees.to_radians() / 2.0).tan();
    for (index, world) in triangle.vertices.iter().enumerate() {
        let delta = [
            world[0] - camera.position[0],
            world[1] - camera.position[1],
            world[2] - camera.position[2],
        ];
        let z = dot(delta, basis.2);
        if z <= 0.05 {
            return;
        }
        p[index] = [
            (dot(delta, basis.0) * focal / aspect / z + 1.0) * 0.5 * width as f32,
            (1.0 - dot(delta, basis.1) * focal / z) * 0.5 * height as f32,
            z,
        ];
    }
    let area = edge(p[0], p[1], p[2][0], p[2][1]);
    if area.abs() < 0.0001 {
        return;
    }
    let min_x = p
        .iter()
        .map(|v| v[0].floor() as i32)
        .min()
        .unwrap_or(0)
        .clamp(0, width as i32 - 1);
    let max_x = p
        .iter()
        .map(|v| v[0].ceil() as i32)
        .max()
        .unwrap_or(0)
        .clamp(0, width as i32 - 1);
    let min_y = p
        .iter()
        .map(|v| v[1].floor() as i32)
        .min()
        .unwrap_or(0)
        .clamp(0, height as i32 - 1);
    let max_y = p
        .iter()
        .map(|v| v[1].ceil() as i32)
        .max()
        .unwrap_or(0)
        .clamp(0, height as i32 - 1);
    for y in min_y..=max_y {
        for x in min_x..=max_x {
            let px = x as f32 + 0.5;
            let py = y as f32 + 0.5;
            let w0 = edge(p[1], p[2], px, py) / area;
            let w1 = edge(p[2], p[0], px, py) / area;
            let w2 = 1.0 - w0 - w1;
            if w0 >= 0.0 && w1 >= 0.0 && w2 >= 0.0 {
                let z = w0 * p[0][2] + w1 * p[1][2] + w2 * p[2][2];
                let index = y as usize * width as usize + x as usize;
                if z < depth[index] {
                    depth[index] = z;
                    for channel in 0..4 {
                        rgba[index * 4 + channel] = (w0
                            * f32::from(triangle.colors_srgb[0][channel])
                            + w1 * f32::from(triangle.colors_srgb[1][channel])
                            + w2 * f32::from(triangle.colors_srgb[2][channel]))
                        .round() as u8;
                    }
                }
            }
        }
    }
}
fn edge(a: [f32; 3], b: [f32; 3], x: f32, y: f32) -> f32 {
    (x - a[0]) * (b[1] - a[1]) - (y - a[1]) * (b[0] - a[0])
}
fn environment_color(scene: &SceneSnapshot) -> [u8; 4] {
    let phase = (scene.observed_unix_millis / 1000 % 86_400) as f32 / 86_400.0;
    let light = (phase * std::f32::consts::TAU)
        .sin()
        .mul_add(20.0, 125.0)
        .clamp(96.0, 160.0) as u8;
    [
        light,
        light.saturating_add(24),
        light.saturating_add(48),
        255,
    ]
}

fn encode_jpeg(
    width: u32,
    height: u32,
    rgba: &[u8],
    maximum: usize,
) -> Result<Vec<u8>, VisionError> {
    let rgb = rgba
        .chunks_exact(4)
        .flat_map(|pixel| pixel[..3].iter().copied())
        .collect::<Vec<_>>();
    let mut output = Vec::new();
    jpeg_encoder::Encoder::new(&mut output, 92)
        .encode(
            &rgb,
            width as u16,
            height as u16,
            jpeg_encoder::ColorType::Rgb,
        )
        .map_err(|_| VisionError::Encode)?;
    if output.len() > maximum {
        return Err(VisionError::ResourceLimit);
    }
    Ok(output)
}
fn scene_summary(scene: &SceneSnapshot) -> String {
    let objects = scene
        .entities
        .iter()
        .filter(|e| e.kind == SceneEntityKind::Object)
        .count();
    let residents = scene
        .entities
        .iter()
        .filter(|e| e.kind == SceneEntityKind::Resident)
        .count();
    let triangles = scene
        .entities
        .iter()
        .map(|entity| entity.triangles.len())
        .sum::<usize>();
    format!(
        "Reconstructed viewport of region {:?}: {objects} tracked objects, {residents} privacy-marked residents, and {triangles} triangles. camera_position={:?}, camera_forward={:?}. generation={} observed_unix_millis={}. completeness: objects_truncated={}, avatars_truncated={}, textures_missing={}, terrain_available={}. This is reconstructed from available scene data, not a viewer framebuffer.",
        sanitize(&scene.region_name),
        scene.camera.position,
        scene.camera.forward,
        scene.generation,
        scene.observed_unix_millis,
        scene.completeness.objects_truncated,
        scene.completeness.avatars_truncated,
        scene.completeness.textures_missing,
        scene.completeness.terrain_available
    )
}
fn sanitize(value: &str) -> String {
    value.chars().filter(|c| !c.is_control()).take(96).collect()
}
fn lock<T>(value: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
    value
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
}

#[cfg(feature = "live-grid")]
pub struct LibremetaverseSceneSource {
    client: libremetaverse::GridClient,
    agent: Arc<libremetaverse::AgentManager>,
    limits: VisionLimits,
    cache: Arc<Mutex<SceneCache>>,
    latest_scene: metacrate_game_loop::Latest<SceneSnapshot>,
    scene_build: tokio::sync::Mutex<()>,
    prefetch_task: Mutex<Option<tokio::task::JoinHandle<()>>>,
    prefetch_passes: AtomicU64,
    scene_sequence: AtomicU64,
}

#[cfg(feature = "live-grid")]
#[derive(Default)]
struct SceneCache {
    region_id: Option<UUID>,
    textures: std::collections::BTreeMap<UUID, CachedTexture>,
    texture_pixels: usize,
    geometry: std::collections::HashMap<UUID, CachedGeometry>,
    legacy_materials: std::collections::BTreeMap<UUID, libremetaverse::materials::LegacyMaterial>,
    pbr_materials: std::collections::BTreeMap<UUID, libremetaverse::assets::AssetMaterial>,
    access: u64,
}

#[cfg(feature = "live-grid")]
struct CachedTexture {
    texture: Arc<DecodedTexture>,
    pixels: usize,
    last_used: u64,
}

#[cfg(feature = "live-grid")]
#[derive(Clone, PartialEq)]
struct GeometrySignature {
    construction: libremetaverse::PrimitiveConstructionData,
    sculpt: Option<libremetaverse::PrimitiveSculptData>,
    textures: Vec<u8>,
    detail: libremetaverse::rendering::DetailLevel,
}

#[cfg(feature = "live-grid")]
struct CachedGeometry {
    signature: GeometrySignature,
    mesh: Arc<libremetaverse::rendering::FacetedMesh>,
    last_used: u64,
}

#[cfg(feature = "live-grid")]
impl LibremetaverseSceneSource {
    #[must_use]
    pub fn new(owner: &crate::backend::LibremetaverseClientOwner, limits: VisionLimits) -> Self {
        Self {
            client: owner.client().clone(),
            agent: owner.agent(),
            limits,
            cache: Arc::new(Mutex::new(SceneCache::default())),
            latest_scene: metacrate_game_loop::Latest::default(),
            scene_build: tokio::sync::Mutex::new(()),
            prefetch_task: Mutex::new(None),
            prefetch_passes: AtomicU64::new(0),
            scene_sequence: AtomicU64::new(0),
        }
    }

    /// Continuously warms the same asset, decode, material, and geometry caches used by capture.
    pub fn start_prefetch(self: &Arc<Self>) {
        let mut task = lock(&self.prefetch_task);
        if task.is_some() {
            return;
        }
        let source = Arc::downgrade(self);
        *task = Some(tokio::spawn(async move {
            loop {
                let Some(source) = source.upgrade() else {
                    break;
                };
                if source.client.network().current_sim().is_some()
                    && let Ok(scene) = source.capture_scene(0, CancellationToken::default()).await
                {
                    if source
                        .latest_scene
                        .load()
                        .is_none_or(|published| !same_render_scene(&published.value, &scene))
                    {
                        source.latest_scene.publish(scene);
                    }
                    source.prefetch_passes.fetch_add(1, Ordering::AcqRel);
                }
                drop(source);
                tokio::time::sleep(Duration::from_secs(1)).await;
            }
        }));
    }

    /// Stops background prefetching before logout or runtime shutdown.
    pub async fn stop_prefetch(&self) {
        let task = lock(&self.prefetch_task).take();
        if let Some(task) = task {
            task.abort();
            let _ = task.await;
        }
    }

    #[must_use]
    pub fn prefetch_passes(&self) -> u64 {
        self.prefetch_passes.load(Ordering::Acquire)
    }

    fn cached_scene(
        &self,
        generation: u64,
        simulator: &libremetaverse::Simulator,
    ) -> Option<SceneSnapshot> {
        let published = self.latest_scene.load()?;
        if generation == 0 || published.value.region_id != simulator.region_id {
            return None;
        }
        let camera = &self.agent.movement.camera;
        let position = camera.position();
        let forward = native_camera_forward(camera);
        let up = camera.up_axis();
        let mut scene = (*published.value).clone();
        scene.generation = generation;
        scene.observed_unix_millis = unix_millis_now();
        scene.camera = CameraPose {
            position: [position.x, position.y, position.z],
            forward: [forward.x, forward.y, forward.z],
            up: [up.x, up.y, up.z],
            vertical_fov_degrees: camera.vertical_fov_angle().to_degrees(),
        };
        scene.timings = SceneBuildTimings::default();
        Some(scene)
    }
}

#[cfg(feature = "live-grid")]
fn same_render_scene(left: &SceneSnapshot, right: &SceneSnapshot) -> bool {
    left.region_id == right.region_id
        && left.entities == right.entities
        && left.textures == right.textures
        && left.completeness == right.completeness
}

#[cfg(feature = "live-grid")]
impl Drop for LibremetaverseSceneSource {
    fn drop(&mut self) {
        if let Some(task) = lock(&self.prefetch_task).take() {
            task.abort();
        }
    }
}

#[cfg(feature = "live-grid")]
async fn request_asset_bytes(
    assets: libremetaverse::AssetManager,
    id: UUID,
    asset_type: libremetaverse_types::AssetType,
    cancellation: CancellationToken,
) -> Option<Vec<u8>> {
    if let Ok(Some(bytes)) = assets.cache.get_cached_asset_bytes_with_uuid(id) {
        return Some(bytes);
    }
    tokio::time::timeout(Duration::from_secs(8), async {
        match asset_type {
            libremetaverse_types::AssetType::Texture => {
                let _ = assets
                    .request_image(id, None, Some(cancellation.clone()))
                    .await;
            }
            libremetaverse_types::AssetType::Mesh => {
                let _ = assets.request_mesh(id, Some(cancellation.clone())).await;
            }
            _ => {}
        }
        assets
            .request_asset_with_uuid_asset_type_boolean_cancellation_token(
                id,
                asset_type,
                false,
                Some(cancellation),
            )
            .await
    })
    .await
    .ok()
    .and_then(Result::ok)
    .flatten()
    .map(|asset| asset.asset_data)
}

#[cfg(feature = "live-grid")]
impl SceneSource for LibremetaverseSceneSource {
    #[allow(clippy::too_many_lines)]
    fn capture_scene(
        &self,
        generation: u64,
        cancellation: CancellationToken,
    ) -> VisionFuture<'_, SceneSnapshot> {
        Box::pin(async move {
            let build_started = Instant::now();
            if cancellation.is_cancellation_requested() {
                return Err(VisionError::Cancelled);
            }
            let simulator = self
                .client
                .network()
                .current_sim()
                .ok_or(VisionError::StaleGeneration)?;
            if let Some(scene) = self.cached_scene(generation, &simulator) {
                return Ok(scene);
            }
            let _scene_build = self.scene_build.lock().await;
            if let Some(scene) = self.cached_scene(generation, &simulator) {
                return Ok(scene);
            }
            let scene_deadline = Instant::now() + Duration::from_secs(5);
            loop {
                let terrain_ready = simulator
                    .terrain
                    .read()
                    .unwrap_or_else(std::sync::PoisonError::into_inner)
                    .iter()
                    .any(|patch| patch.data.len() == 256);
                if terrain_ready {
                    break;
                }
                if Instant::now() >= scene_deadline {
                    return Err(VisionError::TimedOut);
                }
                tokio::select! {
                    () = cancellation.cancelled() => return Err(VisionError::Cancelled),
                    () = tokio::time::sleep(Duration::from_millis(100)) => {}
                }
            }
            let terrain_wait = build_started.elapsed();
            let discovery_started = Instant::now();
            let camera = &self.agent.movement.camera;
            let position = if generation == 0 {
                self.agent.sim_position()
            } else {
                camera.position()
            };
            let forward = native_camera_forward(camera);
            let up = camera.up_axis();
            let entity_limit = self.limits.max_entities.saturating_sub(2);
            let camera_position = [position.x, position.y, position.z];
            let camera_forward = [forward.x, forward.y, forward.z];
            let maximum_distance = self.limits.max_distance_meters as f32;
            let all_avatars = simulator
                .objects_avatars
                .read()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .values()
                .cloned()
                .collect::<Vec<_>>();
            let avatar_parents = all_avatars
                .iter()
                .map(|avatar| (avatar.local_id, (avatar.position, avatar.rotation)))
                .collect::<std::collections::BTreeMap<_, _>>();
            let mut avatars = all_avatars
                .into_iter()
                .filter(|avatar| {
                    scene_center_visible(
                        avatar.position,
                        camera_position,
                        camera_forward,
                        camera.vertical_fov_angle(),
                        self.limits.width as f32 / self.limits.height as f32,
                        maximum_distance,
                    )
                })
                .collect::<Vec<_>>();
            avatars.sort_by(|left, right| {
                scene_distance_squared(left.position, camera_position)
                    .total_cmp(&scene_distance_squared(right.position, camera_position))
            });
            let attachment_avatar_ids = avatars
                .iter()
                .filter(|avatar| {
                    scene_center_focused(
                        avatar.position,
                        camera_position,
                        camera_forward,
                        maximum_distance,
                    )
                })
                .map(|avatar| avatar.local_id)
                .collect::<std::collections::BTreeSet<_>>();
            let (mut world, mut attachments) = {
                let primitive_cache = simulator
                    .objects_primitives
                    .read()
                    .unwrap_or_else(std::sync::PoisonError::into_inner);
                let mut world = Vec::new();
                let mut attachments = Vec::new();
                for prim in primitive_cache.values().cloned() {
                    let Some((prim, attachment_avatar)) =
                        world_primitive(prim, &primitive_cache, &avatar_parents)
                    else {
                        continue;
                    };
                    if let Some(avatar) = attachment_avatar {
                        if attachment_avatar_ids.contains(&avatar) {
                            attachments.push(prim);
                        }
                    } else if scene_within_distance(
                        prim.position,
                        camera_position,
                        maximum_distance,
                    ) {
                        world.push(prim);
                    }
                }
                (world, attachments)
            };
            world.sort_by(|left, right| {
                let left_visible = scene_center_visible(
                    left.position,
                    camera_position,
                    camera_forward,
                    camera.vertical_fov_angle(),
                    self.limits.width as f32 / self.limits.height as f32,
                    maximum_distance,
                );
                let right_visible = scene_center_visible(
                    right.position,
                    camera_position,
                    camera_forward,
                    camera.vertical_fov_angle(),
                    self.limits.width as f32 / self.limits.height as f32,
                    maximum_distance,
                );
                right_visible.cmp(&left_visible).then_with(|| {
                    scene_distance_squared(left.position, camera_position)
                        .total_cmp(&scene_distance_squared(right.position, camera_position))
                })
            });
            attachments.sort_by(|left, right| {
                scene_distance_squared(left.position, camera_position)
                    .total_cmp(&scene_distance_squared(right.position, camera_position))
            });
            let avatar_limit = avatars.len().min(entity_limit);
            let object_limit = entity_limit.saturating_sub(avatar_limit);
            let world_limit = world.len().min(object_limit);
            let attachment_limit = attachments.len().min(object_limit - world_limit);
            let objects_truncated =
                world.len() > world_limit || attachments.len() > attachment_limit;
            let avatars_truncated = avatars.len() > avatar_limit;
            world.truncate(world_limit);
            attachments.truncate(attachment_limit);
            world.extend(attachments);
            let prims = world;
            avatars.truncate(avatar_limit);
            let region_id = simulator.region_id;
            let region_name = simulator.name.clone();
            {
                let mut cache = lock(&self.cache);
                if cache.region_id != Some(region_id) {
                    cache.region_id = Some(region_id);
                    cache.textures.clear();
                    cache.texture_pixels = 0;
                    cache.geometry.clear();
                    cache.legacy_materials.clear();
                    cache.pbr_materials.clear();
                    cache.access = 0;
                }
            }
            let water = simulator.water_height;
            let (region_size_x, region_size_y) = simulator.region_size();
            let region_size = (region_size_x as f32, region_size_y as f32);
            let terrain = simulator
                .terrain
                .read()
                .unwrap_or_else(std::sync::PoisonError::into_inner)
                .clone();
            let terrain_available = terrain.iter().any(|patch| patch.data.len() == 256);
            let terrain_materials = TerrainMaterials {
                texture_ids: [
                    simulator.terrain_detail0,
                    simulator.terrain_detail1,
                    simulator.terrain_detail2,
                    simulator.terrain_detail3,
                ],
                start_heights: [
                    simulator.terrain_start_height00,
                    simulator.terrain_start_height01,
                    simulator.terrain_start_height10,
                    simulator.terrain_start_height11,
                ],
                height_ranges: [
                    simulator.terrain_height_range00,
                    simulator.terrain_height_range01,
                    simulator.terrain_height_range10,
                    simulator.terrain_height_range11,
                ],
            };
            let mut texture_ids = terrain_materials
                .texture_ids
                .into_iter()
                .filter(|id| *id != UUID::zero())
                .collect::<Vec<_>>();
            let mut legacy_material_ids = Vec::new();
            let mut pbr_material_ids = Vec::new();
            for prim in &prims {
                if let Some(sculpt) = &prim.sculpt
                    && sculpt.type_() != libremetaverse_types::SculptType::Mesh
                    && sculpt.sculpt_texture != UUID::zero()
                    && !texture_ids.contains(&sculpt.sculpt_texture)
                {
                    texture_ids.push(sculpt.sculpt_texture);
                }
                if let Some(textures) = &prim.textures {
                    let faces = textures
                        .default_texture
                        .iter()
                        .chain(textures.face_textures.iter().flatten());
                    for face in faces {
                        let id = face.texture_id();
                        if id != UUID::zero() && !texture_ids.contains(&id) {
                            texture_ids.push(id);
                        }
                        let legacy = face.material_id();
                        if legacy != UUID::zero() && !legacy_material_ids.contains(&legacy) {
                            legacy_material_ids.push(legacy);
                        }
                        let pbr = face.render_material_id();
                        if pbr != UUID::zero() && !pbr_material_ids.contains(&pbr) {
                            pbr_material_ids.push(pbr);
                        }
                    }
                }
            }
            let mesh_ids = prims
                .iter()
                .filter(|prim| {
                    let signature = geometry_signature(prim, camera_position);
                    !lock(&self.cache)
                        .geometry
                        .get(&prim.id)
                        .is_some_and(|cached| cached.signature == signature)
                })
                .filter_map(|prim| prim.sculpt.as_ref())
                .filter(|sculpt| sculpt.type_() == libremetaverse_types::SculptType::Mesh)
                .map(|sculpt| sculpt.sculpt_texture)
                .filter(|id| *id != UUID::zero())
                .fold(Vec::new(), |mut ids, id| {
                    // ponytail: stage 256 new meshes per frame; switch to an adaptive
                    // scheduler if live convergence proves this ceiling too slow.
                    if !ids.contains(&id) && ids.len() < 256 {
                        ids.push(id);
                    }
                    ids
                });
            let assets = self.client.assets();
            let object_snapshot_and_asset_discovery = discovery_started.elapsed();
            let material_started = Instant::now();
            let missing_legacy_materials = {
                let cache = lock(&self.cache);
                legacy_material_ids
                    .iter()
                    .any(|id| !cache.legacy_materials.contains_key(id))
            };
            let legacy_material_fetches = usize::from(missing_legacy_materials);
            if missing_legacy_materials {
                let fetched = self
                    .client
                    .objects()
                    .request_materials_with_simulator_cancellation_token(
                        simulator.clone(),
                        Some(cancellation.clone()),
                    )
                    .await
                    .map_or_else(|_| Vec::new(), Iterator::collect::<Vec<_>>);
                let mut cache = lock(&self.cache);
                cache.legacy_materials.extend(
                    fetched
                        .into_iter()
                        .map(|material| (material.id(), material)),
                );
            }
            let legacy_materials = {
                let cache = lock(&self.cache);
                legacy_material_ids
                    .iter()
                    .filter_map(|id| cache.legacy_materials.get(id).cloned())
                    .collect::<Vec<_>>()
            };
            for material in &legacy_materials {
                for id in [material.normal_map(), material.specular_map()] {
                    if id != UUID::zero() && !texture_ids.contains(&id) {
                        texture_ids.push(id);
                    }
                }
            }
            let mut texture_bytes = 0usize;
            let mut pbr_materials = {
                let cache = lock(&self.cache);
                pbr_material_ids
                    .iter()
                    .filter_map(|id| {
                        cache
                            .pbr_materials
                            .get(id)
                            .cloned()
                            .map(|value| (*id, value))
                    })
                    .collect::<Vec<_>>()
            };
            for (_, material) in &pbr_materials {
                for texture in material.texture_ids() {
                    if texture != UUID::zero() && !texture_ids.contains(&texture) {
                        texture_ids.push(texture);
                    }
                }
            }
            {
                let cache = lock(&self.cache);
                pbr_material_ids.retain(|id| !cache.pbr_materials.contains_key(id));
            }
            pbr_material_ids.truncate(self.limits.max_texture_fetches / 4);
            let mut pending_materials = tokio::task::JoinSet::new();
            let mut material_jobs = std::collections::VecDeque::from(pbr_material_ids.clone());
            for _ in 0..16 {
                let Some(id) = material_jobs.pop_front() else {
                    break;
                };
                let assets = assets.clone();
                let token = cancellation.clone();
                pending_materials.spawn(async move {
                    (
                        id,
                        request_asset_bytes(
                            assets,
                            id,
                            libremetaverse_types::AssetType::Material,
                            token,
                        )
                        .await,
                    )
                });
            }
            while let Some(result) = pending_materials.join_next().await {
                if cancellation.is_cancellation_requested() {
                    pending_materials.abort_all();
                    return Err(VisionError::Cancelled);
                }
                if let Ok((id, Some(bytes))) = result
                    && texture_bytes.saturating_add(bytes.len()) <= self.limits.max_texture_bytes
                {
                    texture_bytes += bytes.len();
                    if let Ok(material) =
                        libremetaverse::assets::AssetMaterial::new_with_uuid_bytes(id, bytes)
                    {
                        for texture in material.texture_ids() {
                            if texture != UUID::zero() && !texture_ids.contains(&texture) {
                                texture_ids.push(texture);
                            }
                        }
                        lock(&self.cache).pbr_materials.insert(id, material.clone());
                        pbr_materials.push((id, material));
                    }
                }
                if let Some(id) = material_jobs.pop_front() {
                    let assets = assets.clone();
                    let token = cancellation.clone();
                    pending_materials.spawn(async move {
                        (
                            id,
                            request_asset_bytes(
                                assets,
                                id,
                                libremetaverse_types::AssetType::Material,
                                token,
                            )
                            .await,
                        )
                    });
                }
            }
            let material_fetch = material_started.elapsed();
            let asset_fetch_started = Instant::now();
            texture_ids.retain(|id| *id != UUID::zero());
            let scene_texture_ids = texture_ids.clone();
            {
                let cache = lock(&self.cache);
                texture_ids.retain(|id| !cache.textures.contains_key(id));
            }
            texture_ids.truncate(
                self.limits
                    .max_texture_fetches
                    .saturating_sub(
                        mesh_ids
                            .len()
                            .saturating_add(pbr_material_ids.len())
                            .saturating_add(legacy_material_fetches),
                    )
                    .min(256),
            );
            let texture_fetches = texture_ids
                .len()
                .saturating_add(mesh_ids.len())
                .saturating_add(pbr_material_ids.len())
                .saturating_add(legacy_material_fetches);
            let mut encoded_textures = Vec::new();
            let mut encoded_meshes = Vec::new();
            let mut pending = tokio::task::JoinSet::new();
            let mut asset_jobs = std::collections::VecDeque::new();
            asset_jobs.extend(texture_ids.into_iter().map(|id| (false, id)));
            asset_jobs.extend(mesh_ids.into_iter().map(|id| (true, id)));
            for _ in 0..32 {
                let Some((mesh, id)) = asset_jobs.pop_front() else {
                    break;
                };
                let assets = assets.clone();
                let token = cancellation.clone();
                pending.spawn(async move {
                    (
                        mesh,
                        id,
                        request_asset_bytes(
                            assets,
                            id,
                            if mesh {
                                libremetaverse_types::AssetType::Mesh
                            } else {
                                libremetaverse_types::AssetType::Texture
                            },
                            token,
                        )
                        .await,
                    )
                });
            }
            while let Some(result) = pending.join_next().await {
                if cancellation.is_cancellation_requested() {
                    pending.abort_all();
                    return Err(VisionError::Cancelled);
                }
                if let Ok((mesh, id, Some(bytes))) = result
                    && texture_bytes.saturating_add(bytes.len()) <= self.limits.max_texture_bytes
                {
                    texture_bytes += bytes.len();
                    if mesh {
                        encoded_meshes.push((id, bytes));
                    } else {
                        encoded_textures.push((id, bytes));
                    }
                }
                if let Some((mesh, id)) = asset_jobs.pop_front() {
                    let assets = assets.clone();
                    let token = cancellation.clone();
                    pending.spawn(async move {
                        (
                            mesh,
                            id,
                            request_asset_bytes(
                                assets,
                                id,
                                if mesh {
                                    libremetaverse_types::AssetType::Mesh
                                } else {
                                    libremetaverse_types::AssetType::Texture
                                },
                                token,
                            )
                            .await,
                        )
                    });
                }
            }
            let asset_fetch = asset_fetch_started.elapsed();
            let conversion_started = Instant::now();
            let maximum = self.limits.max_triangles;
            let decode_pixels = self.limits.max_decode_pixels;
            let decode_bytes = self.limits.max_texture_bytes;
            let max_cached_geometry = self.limits.max_entities;
            let cache = Arc::clone(&self.cache);
            let (entities, textures, decoded_texture_pixels, geometry_truncated) =
                tokio::task::spawn_blocking(move || {
                    native_entities(
                        prims,
                        avatars,
                        water,
                        region_size,
                        &terrain,
                        terrain_materials,
                        maximum,
                        encoded_textures,
                        encoded_meshes,
                        legacy_materials,
                        pbr_materials,
                        (decode_bytes, decode_pixels),
                        cache.as_ref(),
                        &scene_texture_ids,
                        max_cached_geometry,
                        camera_position,
                        maximum_distance,
                    )
                })
                .await
                .map_err(|_| VisionError::InvalidScene)??;
            let decode_and_geometry = conversion_started.elapsed();
            if cancellation.is_cancellation_requested() {
                return Err(VisionError::Cancelled);
            }
            let textures_missing = entities
                .iter()
                .filter(|entity| {
                    !entity.texture_available && entity.kind == SceneEntityKind::Object
                })
                .count()
                .try_into()
                .unwrap_or(u32::MAX);
            Ok(SceneSnapshot {
                generation,
                scene_sequence: self.scene_sequence.fetch_add(1, Ordering::AcqRel) + 1,
                observed_unix_millis: unix_millis_now(),
                region_id,
                region_name,
                camera: CameraPose {
                    position: [position.x, position.y, position.z],
                    forward: [forward.x, forward.y, forward.z],
                    up: [up.x, up.y, up.z],
                    vertical_fov_degrees: camera.vertical_fov_angle().to_degrees(),
                },
                entities: entities.into(),
                textures: textures.into(),
                completeness: SnapshotCompleteness {
                    objects_truncated: objects_truncated || geometry_truncated,
                    avatars_truncated,
                    textures_missing,
                    terrain_available,
                },
                texture_fetches,
                texture_bytes,
                decoded_texture_pixels,
                timings: SceneBuildTimings {
                    terrain_wait,
                    object_snapshot_and_asset_discovery,
                    material_fetch,
                    asset_fetch,
                    decode_and_geometry,
                },
            })
        })
    }
}

fn unix_millis_now() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_or(0, |duration| {
            duration.as_millis().try_into().unwrap_or(u64::MAX)
        })
}

#[cfg(feature = "live-grid")]
pub(crate) fn native_camera_forward(
    camera: &libremetaverse::AgentManagerAgentMovementAgentCamera,
) -> libremetaverse_types::Vector3 {
    // LibreMetaverse's AgentCamera wrapper swaps its frame X/Y axes when it
    // writes AgentUpdate; LeftAxis is therefore the actual look direction.
    camera.left_axis()
}

#[cfg(feature = "live-grid")]
pub(crate) fn scene_within_distance(
    position: libremetaverse_types::Vector3,
    camera: [f32; 3],
    maximum: f32,
) -> bool {
    scene_distance_squared(position, camera) <= maximum * maximum
}

#[cfg(feature = "live-grid")]
fn scene_distance_squared(position: libremetaverse_types::Vector3, camera: [f32; 3]) -> f32 {
    let delta = [
        position.x - camera[0],
        position.y - camera[1],
        position.z - camera[2],
    ];
    dot(delta, delta)
}

#[cfg(feature = "live-grid")]
pub(crate) fn scene_center_visible(
    position: libremetaverse_types::Vector3,
    camera: [f32; 3],
    forward: [f32; 3],
    vertical_fov: f32,
    aspect: f32,
    maximum: f32,
) -> bool {
    let delta = [
        position.x - camera[0],
        position.y - camera[1],
        position.z - camera[2],
    ];
    let distance_squared = dot(delta, delta);
    if distance_squared > maximum * maximum {
        return false;
    }
    if distance_squared <= f32::EPSILON {
        return true;
    }
    let horizontal_half_fov = ((vertical_fov * 0.5).tan() * aspect).atan();
    let cosine_limit = horizontal_half_fov.max(vertical_fov * 0.5).cos();
    dot(delta, forward) / distance_squared.sqrt() >= cosine_limit
}

#[cfg(feature = "live-grid")]
pub(crate) fn scene_center_focused(
    position: libremetaverse_types::Vector3,
    camera: [f32; 3],
    forward: [f32; 3],
    maximum: f32,
) -> bool {
    let delta = [
        position.x - camera[0],
        position.y - camera[1],
        position.z - camera[2],
    ];
    let distance_squared = dot(delta, delta);
    distance_squared <= maximum * maximum
        && distance_squared > f32::EPSILON
        && dot(delta, forward) / distance_squared.sqrt() >= 15_f32.to_radians().cos()
}

#[cfg(feature = "live-grid")]
fn world_primitive(
    mut prim: libremetaverse::Primitive,
    primitives: &std::collections::HashMap<u32, libremetaverse::Primitive>,
    avatars: &std::collections::BTreeMap<
        u32,
        (
            libremetaverse_types::Vector3,
            libremetaverse_types::Quaternion,
        ),
    >,
) -> Option<(libremetaverse::Primitive, Option<u32>)> {
    let mut parent_id = prim.parent_id;
    for _ in 0..32 {
        if parent_id == 0 {
            prim.parent_id = 0;
            return Some((prim, None));
        }
        let (parent_position, parent_rotation, next_parent, attachment) =
            if let Some(parent) = primitives.get(&parent_id) {
                (parent.position, parent.rotation, parent.parent_id, None)
            } else if let Some((position, rotation)) = avatars.get(&parent_id) {
                (*position, *rotation, 0, Some(parent_id))
            } else {
                return None;
            };
        prim.position = libremetaverse_types::Vector3::add_with_vector3_vector3(
            parent_position,
            libremetaverse_types::Vector3::mul_with_vector3_quaternion(
                prim.position,
                parent_rotation,
            ),
        )
        .ok()?;
        prim.rotation = libremetaverse_types::Quaternion::mul_with_quaternion_quaternion(
            parent_rotation,
            prim.rotation,
        );
        if let Some(avatar) = attachment {
            prim.parent_id = 0;
            prim.is_attachment = true;
            return Some((prim, Some(avatar)));
        }
        parent_id = next_parent;
    }
    None
}

#[cfg(feature = "live-grid")]
#[derive(Clone, Copy)]
pub(crate) struct TerrainMaterials {
    pub(crate) texture_ids: [UUID; 4],
    pub(crate) start_heights: [f32; 4],
    pub(crate) height_ranges: [f32; 4],
}

#[cfg(feature = "live-grid")]
#[allow(clippy::too_many_arguments, clippy::too_many_lines)] // Single blocking scene-conversion boundary.
fn native_entities(
    prims: Vec<libremetaverse::Primitive>,
    avatars: Vec<libremetaverse::Avatar>,
    water: f32,
    region_size: (f32, f32),
    terrain: &[libremetaverse::TerrainPatch],
    terrain_materials: TerrainMaterials,
    maximum: usize,
    encoded_textures: Vec<(UUID, Vec<u8>)>,
    encoded_meshes: Vec<(UUID, Vec<u8>)>,
    legacy_materials: Vec<libremetaverse::materials::LegacyMaterial>,
    pbr_materials: Vec<(UUID, libremetaverse::assets::AssetMaterial)>,
    decode_limits: (usize, usize),
    cache: &Mutex<SceneCache>,
    scene_texture_ids: &[UUID],
    max_cached_geometry: usize,
    camera_position: [f32; 3],
    maximum_distance: f32,
) -> Result<(Vec<SceneEntity>, Vec<SceneTexture>, usize, bool), VisionError> {
    let (textures, pixels_consumed) = cached_textures(
        cache,
        scene_texture_ids,
        encoded_textures,
        decode_limits.0,
        decode_limits.1,
    );
    let mesh_assets = encoded_meshes
        .into_iter()
        .collect::<std::collections::BTreeMap<_, _>>();
    let texture_indices = textures
        .keys()
        .enumerate()
        .map(|(index, id)| (*id, index))
        .collect::<std::collections::BTreeMap<_, _>>();
    let legacy_materials = legacy_materials
        .into_iter()
        .map(|material| (material.id(), material))
        .collect::<std::collections::BTreeMap<_, _>>();
    let pbr_materials = pbr_materials
        .into_iter()
        .collect::<std::collections::BTreeMap<_, _>>();
    let renderer = libremetaverse_rendering_simple::SimpleRenderer::new()
        .map_err(|_| VisionError::InvalidScene)?;
    let mut entities = Vec::new();
    let mut triangles = 0usize;
    let mut gpu_triangles = 0usize;
    let mut geometry_truncated = false;
    let avatar_triangle_reserve = avatars.len().saturating_mul(2).min(maximum / 4);
    let terrain_triangle_reserve = ((maximum_distance.ceil() as usize).saturating_mul(2))
        .saturating_pow(2)
        .saturating_mul(2)
        .min(maximum / 2);
    let object_triangle_limit = maximum
        .saturating_sub(terrain_triangle_reserve)
        .saturating_sub(avatar_triangle_reserve)
        .saturating_sub(2);
    for prim in prims {
        if triangles >= object_triangle_limit || gpu_triangles >= object_triangle_limit {
            break;
        }
        let name = prim.properties.as_ref().map_or_else(
            || "tracked object".to_owned(),
            |properties| properties.name.clone(),
        );
        let detail = scene_detail_level(&prim, camera_position);
        let signature = geometry_signature(&prim, camera_position);
        let mesh = cached_geometry(cache, prim.id, &signature).or_else(|| {
            let generated = if let Some(sculpt) = &prim.sculpt {
                if sculpt.type_() == libremetaverse_types::SculptType::Mesh {
                    let bytes = mesh_assets.get(&sculpt.sculpt_texture)?;
                    let asset = libremetaverse::assets::AssetMesh::new_with_uuid_bytes(
                        sculpt.sculpt_texture,
                        bytes.clone(),
                    )
                    .ok()?;
                    let mut decoded = None;
                    if !libremetaverse::rendering::FacetedMesh::try_decode_from_asset(
                        prim.clone(),
                        asset,
                        detail,
                        &mut decoded,
                    ) {
                        return None;
                    }
                    decoded?
                } else {
                    let image = textures.get(&sculpt.sculpt_texture)?.managed_image().ok()?;
                    renderer
                        .generate_faceted_sculpt_mesh(prim.clone(), image, detail)
                        .ok()?
                }
            } else {
                renderer.generate_faceted_mesh(prim.clone(), detail).ok()?
            };
            Some(store_geometry(
                cache,
                prim.id,
                signature,
                generated,
                max_cached_geometry,
            ))
        });
        let Some(mesh) = mesh else { continue };
        let object_triangles = mesh
            .faces
            .iter()
            .map(|face| {
                face.indices
                    .chunks_exact(3)
                    .filter(|indices| {
                        indices
                            .iter()
                            .all(|index| usize::from(*index) < face.vertices.len())
                    })
                    .count()
            })
            .sum::<usize>();
        if gpu_triangles.saturating_add(object_triangles) > object_triangle_limit {
            geometry_truncated = true;
            continue;
        }
        let mut output = Vec::new();
        let mut renderables = Vec::new();
        let mut texture_complete = true;
        for face in &mesh.faces {
            let rgba = face.texture_face.rgba();
            let tint = [
                unit_byte(rgba.r),
                unit_byte(rgba.g),
                unit_byte(rgba.b),
                unit_byte(rgba.a),
            ];
            let texture = textures.get(&face.texture_face.texture_id());
            if face.texture_face.texture_id() != UUID::zero() && texture.is_none() {
                texture_complete = false;
            }
            let gpu_vertices = face
                .vertices
                .iter()
                .map(|vertex| native_gpu_vertex(vertex, &prim))
                .collect::<Vec<_>>();
            let gpu_indices = face
                .indices
                .chunks_exact(3)
                .filter(|indices| {
                    indices
                        .iter()
                        .all(|index| usize::from(*index) < gpu_vertices.len())
                })
                .flatten()
                .map(|index| u32::from(*index))
                .collect::<Vec<_>>();
            if !gpu_indices.is_empty() {
                gpu_triangles += gpu_indices.len() / 3;
                renderables.push(SceneRenderable {
                    mesh: metacrate_rendering_wgpu::Mesh {
                        vertices: gpu_vertices,
                        indices: gpu_indices,
                    },
                    material: face_material(
                        &face.texture_face,
                        &texture_indices,
                        &legacy_materials,
                        &pbr_materials,
                        texture.is_some_and(|texture| texture.has_alpha()),
                    ),
                });
            }
            'triangle: for indices in face.indices.chunks_exact(3) {
                let mut vertices = [[0.0; 3]; 3];
                let mut colors = [tint; 3];
                for (index, source) in indices.iter().enumerate() {
                    let Some(vertex) = face.vertices.get(*source as usize) else {
                        continue 'triangle;
                    };
                    let local = vertex.position;
                    let scaled = libremetaverse_types::Vector3 {
                        x: local.x * prim.scale.x,
                        y: local.y * prim.scale.y,
                        z: local.z * prim.scale.z,
                    };
                    let rotated = rotate_vector(scaled, prim.rotation);
                    vertices[index] = [
                        rotated.x + prim.position.x,
                        rotated.y + prim.position.y,
                        rotated.z + prim.position.z,
                    ];
                    if let Some(texture) = texture {
                        colors[index] = multiply_color(
                            tint,
                            texture.sample(vertex.tex_coord.x, vertex.tex_coord.y),
                        );
                    }
                }
                if vertices.iter().flatten().any(|value| !value.is_finite()) {
                    continue;
                }
                let shade = triangle_shade(vertices);
                colors = colors.map(|color| shade_color(color, shade));
                triangles += 1;
                output.push(SceneTriangle {
                    vertices,
                    colors_srgb: colors,
                });
            }
        }
        entities.push(SceneEntity {
            id: prim.id,
            kind: SceneEntityKind::Object,
            display_name: name,
            triangles: output,
            renderables,
            texture_available: texture_complete,
        });
    }
    for avatar in avatars {
        if triangles.saturating_add(2) > maximum / 2 {
            break;
        }
        let p = avatar.position;
        let half = 0.3;
        let bottom = p.z;
        let top = p.z + 1.8;
        let front = p.y;
        let avatar_triangles = vec![
            SceneTriangle {
                vertices: [
                    [p.x - half, front, bottom],
                    [p.x + half, front, bottom],
                    [p.x + half, front, top],
                ],
                colors_srgb: [[80, 140, 220, 255]; 3],
            },
            SceneTriangle {
                vertices: [
                    [p.x - half, front, bottom],
                    [p.x + half, front, top],
                    [p.x - half, front, top],
                ],
                colors_srgb: [[80, 140, 220, 255]; 3],
            },
        ];
        let renderables = scene_renderable_from_triangles(&avatar_triangles)
            .into_iter()
            .collect();
        triangles += 2;
        entities.push(SceneEntity {
            id: avatar.id,
            kind: SceneEntityKind::Resident,
            display_name: format!(
                "resident-{}",
                crate::observability::pseudonymous_identifier(&avatar.id.to_string())
            ),
            triangles: avatar_triangles,
            renderables,
            texture_available: false,
        });
    }
    let terrain_triangles = terrain_triangles(
        terrain,
        region_size,
        water,
        &textures,
        terrain_materials,
        camera_position,
        maximum_distance,
        maximum.saturating_sub(triangles).saturating_sub(2),
    );
    if !terrain_triangles.is_empty() {
        let renderables = scene_renderable_from_triangles(&terrain_triangles)
            .into_iter()
            .collect();
        entities.push(SceneEntity {
            id: UUID::zero(),
            kind: SceneEntityKind::Terrain,
            display_name: "simulator terrain".into(),
            triangles: terrain_triangles,
            renderables,
            texture_available: terrain_materials
                .texture_ids
                .iter()
                .any(|id| textures.contains_key(id)),
        });
    }
    let water_triangles = vec![
        SceneTriangle {
            vertices: [
                [0.0, 0.0, water],
                [region_size.0, 0.0, water],
                [region_size.0, region_size.1, water],
            ],
            colors_srgb: [[35, 105, 165, 105]; 3],
        },
        SceneTriangle {
            vertices: [
                [0.0, 0.0, water],
                [region_size.0, region_size.1, water],
                [0.0, region_size.1, water],
            ],
            colors_srgb: [[35, 105, 165, 105]; 3],
        },
    ];
    entities.push(SceneEntity {
        id: UUID::zero(),
        kind: SceneEntityKind::Environment,
        display_name: "simulator water".into(),
        renderables: scene_renderable_from_triangles(&water_triangles)
            .into_iter()
            .collect(),
        triangles: water_triangles,
        texture_available: true,
    });
    let gpu_textures = textures
        .values()
        .map(|texture| texture.scene_texture())
        .collect();
    Ok((entities, gpu_textures, pixels_consumed, geometry_truncated))
}

#[cfg(feature = "live-grid")]
fn cached_geometry(
    cache: &Mutex<SceneCache>,
    id: UUID,
    signature: &GeometrySignature,
) -> Option<Arc<libremetaverse::rendering::FacetedMesh>> {
    let mut cache = lock(cache);
    cache.access = cache.access.wrapping_add(1);
    let access = cache.access;
    let entry = cache.geometry.get_mut(&id)?;
    if entry.signature != *signature {
        return None;
    }
    entry.last_used = access;
    Some(Arc::clone(&entry.mesh))
}

#[cfg(feature = "live-grid")]
fn store_geometry(
    cache: &Mutex<SceneCache>,
    id: UUID,
    signature: GeometrySignature,
    mesh: libremetaverse::rendering::FacetedMesh,
    maximum: usize,
) -> Arc<libremetaverse::rendering::FacetedMesh> {
    let mesh = Arc::new(mesh);
    let mut cache = lock(cache);
    cache.access = cache.access.wrapping_add(1);
    if !cache.geometry.contains_key(&id)
        && cache.geometry.len() >= maximum
        && let Some(oldest) = cache
            .geometry
            .iter()
            .min_by_key(|(_, geometry)| geometry.last_used)
            .map(|(id, _)| *id)
    {
        cache.geometry.remove(&oldest);
    }
    let last_used = cache.access;
    cache.geometry.insert(
        id,
        CachedGeometry {
            signature,
            mesh: Arc::clone(&mesh),
            last_used,
        },
    );
    mesh
}

#[cfg(feature = "live-grid")]
fn geometry_signature(prim: &libremetaverse::Primitive, camera: [f32; 3]) -> GeometrySignature {
    GeometrySignature {
        construction: prim.prim_data.clone(),
        sculpt: prim.sculpt.clone(),
        textures: prim
            .textures
            .as_ref()
            .and_then(|textures| textures.get_bytes().ok())
            .unwrap_or_default(),
        detail: scene_detail_level(prim, camera),
    }
}

#[cfg(feature = "live-grid")]
pub(crate) fn scene_detail_level(
    prim: &libremetaverse::Primitive,
    camera: [f32; 3],
) -> libremetaverse::rendering::DetailLevel {
    let distance = scene_distance_squared(prim.position, camera).sqrt();
    let extent = prim.scale.x.max(prim.scale.y).max(prim.scale.z);
    if distance < 32.0 || extent >= 16.0 {
        libremetaverse::rendering::DetailLevel::Highest
    } else {
        libremetaverse::rendering::DetailLevel::High
    }
}

#[cfg(feature = "live-grid")]
fn native_gpu_vertex(
    vertex: &libremetaverse::rendering::Vertex,
    prim: &libremetaverse::Primitive,
) -> metacrate_rendering_wgpu::Vertex {
    let scaled = libremetaverse_types::Vector3 {
        x: vertex.position.x * prim.scale.x,
        y: vertex.position.y * prim.scale.y,
        z: vertex.position.z * prim.scale.z,
    };
    let position = rotate_vector(scaled, prim.rotation);
    let inverse_scaled_normal = libremetaverse_types::Vector3 {
        x: vertex.normal.x / prim.scale.x.abs().max(f32::EPSILON),
        y: vertex.normal.y / prim.scale.y.abs().max(f32::EPSILON),
        z: vertex.normal.z / prim.scale.z.abs().max(f32::EPSILON),
    };
    let normal = rotate_vector(inverse_scaled_normal, prim.rotation);
    let normal_length = (normal.x * normal.x + normal.y * normal.y + normal.z * normal.z)
        .sqrt()
        .max(f32::EPSILON);
    metacrate_rendering_wgpu::Vertex {
        position: [
            position.x + prim.position.x,
            position.y + prim.position.y,
            position.z + prim.position.z,
        ],
        normal: [
            normal.x / normal_length,
            normal.y / normal_length,
            normal.z / normal_length,
        ],
        tex_coord: [vertex.tex_coord.x, vertex.tex_coord.y],
        color_srgb: [255; 4],
    }
}

#[cfg(feature = "live-grid")]
#[allow(clippy::too_many_lines)] // Direct mapping of the two supported material models.
pub(crate) fn face_material(
    face: &libremetaverse::PrimitiveTextureEntryFace,
    textures: &std::collections::BTreeMap<UUID, usize>,
    legacy_materials: &std::collections::BTreeMap<UUID, libremetaverse::materials::LegacyMaterial>,
    pbr_materials: &std::collections::BTreeMap<UUID, libremetaverse::assets::AssetMaterial>,
    texture_has_alpha: bool,
) -> metacrate_rendering_wgpu::Material {
    use metacrate_rendering_wgpu::{
        AlphaMode, BlinnPhongMaterial, Material, PbrMaterial, TextureSlot, UvTransform,
    };
    if let Some(material) = pbr_materials.get(&face.render_material_id()) {
        let ids = material.texture_ids();
        let transforms = material.texture_transforms();
        let slot = |index: usize| TextureSlot {
            texture: ids.get(index).and_then(|id| textures.get(id)).copied(),
            transform: transforms
                .get(index)
                .map_or_else(UvTransform::default, |transform| UvTransform {
                    scale: [transform.scale.x, transform.scale.y],
                    offset: [transform.offset.x, transform.offset.y],
                    rotation_radians: transform.rotation,
                }),
        };
        let base = material.base_color_factor();
        let tint = face.rgba();
        let emissive = material.emissive_factor();
        return Material::Pbr(PbrMaterial {
            base_color_srgb: [
                unit_byte(base.r * tint.r),
                unit_byte(base.g * tint.g),
                unit_byte(base.b * tint.b),
                unit_byte(base.a * tint.a),
            ],
            base_color: slot(libremetaverse::assets::AssetMaterial::TEXTURE_BASE_COLOR as usize),
            normal: slot(libremetaverse::assets::AssetMaterial::TEXTURE_NORMAL as usize),
            metallic_roughness: slot(
                libremetaverse::assets::AssetMaterial::TEXTURE_METALLIC_ROUGHNESS as usize,
            ),
            emissive: slot(libremetaverse::assets::AssetMaterial::TEXTURE_EMISSIVE as usize),
            metallic_factor: material.metallic_factor(),
            roughness_factor: material.roughness_factor(),
            emissive_factor_srgb: [
                unit_byte(emissive.x),
                unit_byte(emissive.y),
                unit_byte(emissive.z),
            ],
            double_sided: material.double_sided(),
            alpha_mode: match material.alpha_mode() {
                libremetaverse::assets::GltfAlphaMode::Opaque if tint.a < 0.999 => AlphaMode::Blend,
                libremetaverse::assets::GltfAlphaMode::Opaque => AlphaMode::Opaque,
                libremetaverse::assets::GltfAlphaMode::Blend => AlphaMode::Blend,
                libremetaverse::assets::GltfAlphaMode::Mask => AlphaMode::Mask {
                    cutoff: material.alpha_cutoff(),
                },
            },
        });
    }
    let rgba = face.rgba();
    let automatic_alpha = || {
        if rgba.a < 0.999 {
            AlphaMode::Blend
        } else if texture_has_alpha {
            AlphaMode::Mask { cutoff: 0.5 }
        } else {
            AlphaMode::Opaque
        }
    };
    let legacy = legacy_materials.get(&face.material_id());
    let shininess = match face.shiny() {
        libremetaverse::Shininess::None => 1.0,
        libremetaverse::Shininess::Low => 24.0,
        libremetaverse::Shininess::Medium => 64.0,
        libremetaverse::Shininess::High => 128.0,
    };
    Material::BlinnPhong(BlinnPhongMaterial {
        diffuse_color_srgb: [
            unit_byte(rgba.r),
            unit_byte(rgba.g),
            unit_byte(rgba.b),
            unit_byte(rgba.a),
        ],
        diffuse: TextureSlot {
            texture: textures.get(&face.texture_id()).copied(),
            transform: UvTransform {
                scale: [face.repeat_u(), face.repeat_v()],
                offset: [face.offset_u(), face.offset_v()],
                rotation_radians: face.rotation(),
            },
        },
        normal: legacy.map_or_else(TextureSlot::default, |material| TextureSlot {
            texture: textures.get(&material.normal_map()).copied(),
            transform: UvTransform {
                scale: [
                    material.normal_map_repeat_x() as f32,
                    material.normal_map_repeat_y() as f32,
                ],
                offset: [
                    material.normal_map_offset_x() as f32,
                    material.normal_map_offset_y() as f32,
                ],
                rotation_radians: material.normal_map_rotation() as f32,
            },
        }),
        specular: legacy.map_or_else(TextureSlot::default, |material| TextureSlot {
            texture: textures.get(&material.specular_map()).copied(),
            transform: UvTransform {
                scale: [
                    material.specular_map_repeat_x() as f32,
                    material.specular_map_repeat_y() as f32,
                ],
                offset: [
                    material.specular_map_offset_x() as f32,
                    material.specular_map_offset_y() as f32,
                ],
                rotation_radians: material.specular_map_rotation() as f32,
            },
        }),
        specular_color_srgb: legacy.map_or_else(
            || match face.shiny() {
                libremetaverse::Shininess::None => [0; 3],
                _ => [255; 3],
            },
            |material| {
                let color = material.specular_color();
                [unit_byte(color.r), unit_byte(color.g), unit_byte(color.b)]
            },
        ),
        shininess: legacy.map_or(shininess, |material| {
            f32::from(material.specular_exponent()).max(1.0)
        }),
        environment_intensity: legacy.map_or(0.0, |material| {
            f32::from(material.environment_intensity()) / 255.0
        }),
        fullbright: face.fullbright()
            || legacy.is_some_and(|material| {
                material.diffuse_alpha_mode()
                    == libremetaverse::materials::LegacyMaterialAlphaMode::Emissive
            }),
        alpha_mode: legacy.map_or_else(automatic_alpha, |material| {
            if rgba.a < 0.999 {
                AlphaMode::Blend
            } else {
                match material.diffuse_alpha_mode() {
                    libremetaverse::materials::LegacyMaterialAlphaMode::Blend
                    | libremetaverse::materials::LegacyMaterialAlphaMode::Emissive => {
                        AlphaMode::Blend
                    }
                    libremetaverse::materials::LegacyMaterialAlphaMode::Mask => AlphaMode::Mask {
                        cutoff: f32::from(material.alpha_mask_cutoff()) / 255.0,
                    },
                    libremetaverse::materials::LegacyMaterialAlphaMode::Default
                    | libremetaverse::materials::LegacyMaterialAlphaMode::None => automatic_alpha(),
                }
            }
        }),
        alpha_cutoff: legacy.map_or_else(
            || texture_has_alpha.then_some(0.5),
            |material| {
                if material.diffuse_alpha_mode()
                    == libremetaverse::materials::LegacyMaterialAlphaMode::Mask
                {
                    Some(f32::from(material.alpha_mask_cutoff()) / 255.0)
                } else {
                    texture_has_alpha.then_some(0.5)
                }
            },
        ),
        ..Default::default()
    })
}

#[cfg(feature = "live-grid")]
pub(crate) fn scene_renderable_from_triangles(
    triangles: &[SceneTriangle],
) -> Option<SceneRenderable> {
    if triangles.is_empty() {
        return None;
    }
    let mut vertices = Vec::with_capacity(triangles.len().saturating_mul(3));
    for triangle in triangles {
        let edge_a =
            std::array::from_fn(|axis| triangle.vertices[1][axis] - triangle.vertices[0][axis]);
        let edge_b =
            std::array::from_fn(|axis| triangle.vertices[2][axis] - triangle.vertices[0][axis]);
        let normal = normalize(cross(edge_a, edge_b)).unwrap_or([0.0, 0.0, 1.0]);
        vertices.extend((0..3).map(|index| metacrate_rendering_wgpu::Vertex {
            position: triangle.vertices[index],
            normal,
            tex_coord: [0.0; 2],
            color_srgb: triangle.colors_srgb[index],
        }));
    }
    let indices = (0..vertices.len())
        .map_while(|index| u32::try_from(index).ok())
        .collect();
    Some(SceneRenderable {
        mesh: metacrate_rendering_wgpu::Mesh { vertices, indices },
        material: metacrate_rendering_wgpu::Material::BlinnPhong(
            metacrate_rendering_wgpu::BlinnPhongMaterial {
                fullbright: true,
                double_sided: true,
                alpha_mode: if triangles
                    .iter()
                    .flat_map(|triangle| triangle.colors_srgb)
                    .any(|color| color[3] != 255)
                {
                    metacrate_rendering_wgpu::AlphaMode::Blend
                } else {
                    metacrate_rendering_wgpu::AlphaMode::Opaque
                },
                ..Default::default()
            },
        ),
    })
}

#[cfg(feature = "live-grid")]
pub(crate) struct DecodedTexture {
    width: usize,
    height: usize,
    red: Vec<u8>,
    green: Vec<u8>,
    blue: Vec<u8>,
    alpha: Vec<u8>,
}

#[cfg(feature = "live-grid")]
impl DecodedTexture {
    fn sample(&self, u: f32, v: f32) -> [u8; 4] {
        let x = (u.rem_euclid(1.0) * self.width as f32) as usize % self.width;
        let y = ((1.0 - v).rem_euclid(1.0) * self.height as f32) as usize % self.height;
        let index = y * self.width + x;
        [
            self.red[index],
            self.green.get(index).copied().unwrap_or(self.red[index]),
            self.blue.get(index).copied().unwrap_or(self.red[index]),
            self.alpha.get(index).copied().unwrap_or(255),
        ]
    }

    fn has_alpha(&self) -> bool {
        self.alpha.iter().any(|alpha| *alpha != 255)
    }

    fn scene_texture(&self) -> SceneTexture {
        let pixels = self.width.saturating_mul(self.height);
        let mut rgba = Vec::with_capacity(pixels.saturating_mul(4));
        for index in 0..pixels {
            rgba.extend_from_slice(&[
                self.red[index],
                self.green.get(index).copied().unwrap_or(self.red[index]),
                self.blue.get(index).copied().unwrap_or(self.red[index]),
                self.alpha.get(index).copied().unwrap_or(255),
            ]);
        }
        SceneTexture {
            width: self.width.try_into().unwrap_or(u32::MAX),
            height: self.height.try_into().unwrap_or(u32::MAX),
            rgba,
        }
    }

    fn managed_image(&self) -> Result<libremetaverse_imaging::ManagedImage, VisionError> {
        let width = self
            .width
            .try_into()
            .map_err(|_| VisionError::InvalidScene)?;
        let height = self
            .height
            .try_into()
            .map_err(|_| VisionError::InvalidScene)?;
        let channels = libremetaverse_imaging::ManagedImageImageChannels::COLOR
            | if self.alpha.is_empty() {
                libremetaverse_imaging::ManagedImageImageChannels::default()
            } else {
                libremetaverse_imaging::ManagedImageImageChannels::ALPHA
            };
        let image = libremetaverse_imaging::ManagedImage {
            alpha: self.alpha.clone(),
            blue: self.blue.clone(),
            bump: Vec::new(),
            channels,
            green: self.green.clone(),
            height,
            red: self.red.clone(),
            width,
        };
        image.validate().map_err(|_| VisionError::InvalidScene)?;
        Ok(image)
    }
}

#[cfg(feature = "live-grid")]
fn cached_textures(
    cache: &Mutex<SceneCache>,
    wanted: &[UUID],
    encoded: Vec<(UUID, Vec<u8>)>,
    max_bytes: usize,
    max_cached_pixels: usize,
) -> (std::collections::BTreeMap<UUID, Arc<DecodedTexture>>, usize) {
    const MAX_PIXELS_PER_CAPTURE: usize = 67_108_864;
    let (decoded, _) = decode_textures(
        encoded,
        max_bytes,
        max_cached_pixels.min(MAX_PIXELS_PER_CAPTURE),
    );
    let mut cache = lock(cache);
    store_decoded_textures(&mut cache, wanted, decoded, max_cached_pixels);
    let textures = wanted
        .iter()
        .filter_map(|id| {
            cache
                .textures
                .get(id)
                .map(|texture| (*id, Arc::clone(&texture.texture)))
        })
        .collect::<std::collections::BTreeMap<_, _>>();
    let pixels = textures
        .values()
        .map(|texture| texture.width.saturating_mul(texture.height))
        .sum();
    (textures, pixels)
}

#[cfg(feature = "live-grid")]
fn store_decoded_textures(
    cache: &mut SceneCache,
    wanted: &[UUID],
    decoded: std::collections::BTreeMap<UUID, DecodedTexture>,
    max_cached_pixels: usize,
) {
    cache.access = cache.access.wrapping_add(1);
    let access = cache.access;
    for id in wanted {
        if let Some(entry) = cache.textures.get_mut(id) {
            entry.last_used = access;
        }
    }
    for (id, texture) in decoded {
        let pixels = texture.width.saturating_mul(texture.height);
        if cache.textures.contains_key(&id) || pixels > max_cached_pixels {
            continue;
        }
        while cache.texture_pixels.saturating_add(pixels) > max_cached_pixels {
            let Some(oldest) = cache
                .textures
                .iter()
                .filter(|(id, _)| !wanted.contains(id))
                .min_by_key(|(_, texture)| texture.last_used)
                .map(|(id, _)| *id)
            else {
                break;
            };
            if let Some(removed) = cache.textures.remove(&oldest) {
                cache.texture_pixels = cache.texture_pixels.saturating_sub(removed.pixels);
            }
        }
        if cache.texture_pixels.saturating_add(pixels) > max_cached_pixels {
            continue;
        }
        cache.texture_pixels += pixels;
        cache.textures.insert(
            id,
            CachedTexture {
                texture: Arc::new(texture),
                pixels,
                last_used: access,
            },
        );
    }
}

#[cfg(all(test, feature = "live-grid"))]
mod texture_cache_tests {
    use super::*;

    #[test]
    fn evicts_content_outside_the_current_scene() {
        let texture = || DecodedTexture {
            width: 2,
            height: 2,
            red: vec![0; 4],
            green: vec![0; 4],
            blue: vec![0; 4],
            alpha: vec![255; 4],
        };
        let old = UUID::new_with_string("00000000-0000-4000-8000-000000000001".to_owned()).unwrap();
        let current =
            UUID::new_with_string("00000000-0000-4000-8000-000000000002".to_owned()).unwrap();
        let mut cache = SceneCache::default();
        store_decoded_textures(
            &mut cache,
            &[old],
            std::collections::BTreeMap::from([(old, texture())]),
            4,
        );
        store_decoded_textures(
            &mut cache,
            &[current],
            std::collections::BTreeMap::from([(current, texture())]),
            4,
        );
        assert!(!cache.textures.contains_key(&old));
        assert!(cache.textures.contains_key(&current));
        assert_eq!(cache.texture_pixels, 4);
    }
}

#[cfg(feature = "live-grid")]
fn decode_textures(
    textures: Vec<(UUID, Vec<u8>)>,
    max_bytes: usize,
    max_pixels: usize,
) -> (std::collections::BTreeMap<UUID, DecodedTexture>, usize) {
    let mut output = std::collections::BTreeMap::new();
    let mut remaining_pixels = max_pixels;
    for (id, bytes) in textures {
        if remaining_pixels == 0 {
            break;
        }
        let options = libremetaverse_imaging::J2kDecodeOptions::default()
            .with_limits(max_bytes.min(bytes.len().max(1)), 33_554_432);
        let Ok(mut image) = libremetaverse_imaging::RustJ2kCodec::decode_bytes(&bytes, options)
        else {
            continue;
        };
        // A 512px source still exceeds the screen footprint of almost every
        // face in an agent snapshot, while letting dense UGC scenes converge
        // without retaining gigabytes of decoded 1024px textures.
        if fit_texture_for_view(&mut image, 512).is_err() {
            continue;
        }
        let Ok(width) = usize::try_from(image.width) else {
            continue;
        };
        let Ok(height) = usize::try_from(image.height) else {
            continue;
        };
        let Some(pixels) = width.checked_mul(height) else {
            continue;
        };
        if pixels == 0 || pixels > remaining_pixels || image.red.len() != pixels {
            continue;
        }
        output.insert(
            id,
            DecodedTexture {
                width,
                height,
                red: image.red,
                green: image.green,
                blue: image.blue,
                alpha: image.alpha,
            },
        );
        remaining_pixels -= pixels;
    }
    (output, max_pixels - remaining_pixels)
}

#[cfg(feature = "live-grid")]
pub(crate) fn fit_texture_for_view(
    image: &mut libremetaverse_imaging::ManagedImage,
    maximum_edge: i32,
) -> Result<(), libremetaverse_types::Error> {
    let largest = image.width.max(image.height);
    if largest <= maximum_edge {
        return Ok(());
    }
    let width = (i64::from(image.width) * i64::from(maximum_edge) / i64::from(largest))
        .max(1)
        .try_into()
        .map_err(|_| libremetaverse_types::Error::Argument)?;
    let height = (i64::from(image.height) * i64::from(maximum_edge) / i64::from(largest))
        .max(1)
        .try_into()
        .map_err(|_| libremetaverse_types::Error::Argument)?;
    if !image.alpha.is_empty() {
        for color in [&mut image.red, &mut image.green, &mut image.blue] {
            for (sample, alpha) in color.iter_mut().zip(&image.alpha) {
                *sample = u8::try_from(u16::from(*sample) * u16::from(*alpha) / 255)
                    .expect("premultiplied color fits in a byte");
            }
        }
    }
    image.resize_bilinear(width, height)?;
    if !image.alpha.is_empty() {
        for color in [&mut image.red, &mut image.green, &mut image.blue] {
            for (sample, alpha) in color.iter_mut().zip(&image.alpha) {
                if *alpha != 0 {
                    *sample = (u16::from(*sample) * 255 / u16::from(*alpha)).min(255) as u8;
                }
            }
        }
    }
    Ok(())
}

#[cfg(feature = "live-grid")]
fn multiply_color(tint: [u8; 4], texture: [u8; 4]) -> [u8; 4] {
    std::array::from_fn(|channel| {
        ((u16::from(tint[channel]) * u16::from(texture[channel])) / 255) as u8
    })
}

#[cfg(feature = "live-grid")]
pub(crate) fn terrain_height(
    terrain: &[libremetaverse::TerrainPatch],
    patch_width: usize,
    x: usize,
    y: usize,
) -> Option<f32> {
    let patch = terrain.get((y / 16) * patch_width + x / 16)?;
    patch.data.get((y % 16) * 16 + x % 16).copied()
}

#[cfg(feature = "live-grid")]
#[allow(clippy::too_many_arguments)]
pub(crate) fn terrain_triangles(
    terrain: &[libremetaverse::TerrainPatch],
    region_size: (f32, f32),
    water: f32,
    textures: &std::collections::BTreeMap<UUID, Arc<DecodedTexture>>,
    materials: TerrainMaterials,
    camera_position: [f32; 3],
    maximum_distance: f32,
    maximum: usize,
) -> Vec<SceneTriangle> {
    let (width, height, patch_width) = terrain_layout(terrain, region_size);
    if maximum < 2 || width < 2 || height < 2 {
        return Vec::new();
    }
    let radius = maximum_distance.ceil();
    let minimum_x = (camera_position[0] - radius).floor().max(0.0) as usize;
    let minimum_y = (camera_position[1] - radius).floor().max(0.0) as usize;
    let maximum_x = (camera_position[0] + radius).ceil().max(0.0) as usize;
    let maximum_y = (camera_position[1] + radius).ceil().max(0.0) as usize;
    let minimum_x = minimum_x.min(width - 1);
    let minimum_y = minimum_y.min(height - 1);
    let maximum_x = maximum_x.min(width - 1);
    let maximum_y = maximum_y.min(height - 1);
    let mut step = 1usize;
    while (maximum_x - minimum_x).div_ceil(step) * (maximum_y - minimum_y).div_ceil(step) * 2
        > maximum
    {
        step *= 2;
    }
    let color = |x: usize, y: usize, z: f32| {
        let east = x as f32 / region_size.0.max(1.0);
        let north = y as f32 / region_size.1.max(1.0);
        let start = bilinear_corners(materials.start_heights, east, north);
        let range = bilinear_corners(materials.height_ranges, east, north).max(1.0);
        let layer = ((z - start) / range * 3.0).clamp(0.0, 3.0);
        let lower = layer.floor() as usize;
        let upper = layer.ceil() as usize;
        let blend = layer.fract() * layer.fract() * (3.0 - 2.0 * layer.fract());
        let sample = |index: usize| {
            textures.get(&materials.texture_ids[index]).map_or_else(
                || {
                    if z <= water {
                        [45, 90, 125, 255]
                    } else {
                        [
                            [80, 105, 55, 255],
                            [105, 100, 65, 255],
                            [90, 95, 80, 255],
                            [125, 125, 115, 255],
                        ][index]
                    }
                },
                |texture| texture.sample(x as f32 / 12.5, y as f32 / 12.5),
            )
        };
        blend_color(sample(lower), sample(upper), blend)
    };
    let mut output = Vec::new();
    for y in (minimum_y..maximum_y).step_by(step) {
        for x in (minimum_x..maximum_x).step_by(step) {
            if output.len().saturating_add(2) > maximum {
                return output;
            }
            let x1 = (x + step).min(width - 1);
            let y1 = (y + step).min(height - 1);
            let Some(h00) = terrain_height(terrain, patch_width, x, y) else {
                continue;
            };
            let Some(h10) = terrain_height(terrain, patch_width, x1, y) else {
                continue;
            };
            let Some(h11) = terrain_height(terrain, patch_width, x1, y1) else {
                continue;
            };
            let Some(h01) = terrain_height(terrain, patch_width, x, y1) else {
                continue;
            };
            let p00 = [x as f32, y as f32, h00];
            let p10 = [x1 as f32, y as f32, h10];
            let p11 = [x1 as f32, y1 as f32, h11];
            let p01 = [x as f32, y1 as f32, h01];
            let first_vertices = [p00, p10, p11];
            let second_vertices = [p00, p11, p01];
            output.push(SceneTriangle {
                vertices: first_vertices,
                colors_srgb: [color(x, y, h00), color(x1, y, h10), color(x1, y1, h11)]
                    .map(|color| shade_color(color, triangle_shade(first_vertices))),
            });
            output.push(SceneTriangle {
                vertices: second_vertices,
                colors_srgb: [color(x, y, h00), color(x1, y1, h11), color(x, y1, h01)]
                    .map(|color| shade_color(color, triangle_shade(second_vertices))),
            });
        }
    }
    output
}

#[cfg(feature = "live-grid")]
pub(crate) fn bilinear_corners(values: [f32; 4], east: f32, north: f32) -> f32 {
    let east = east.clamp(0.0, 1.0);
    let north = north.clamp(0.0, 1.0);
    let south = values[0] * (1.0 - east) + values[2] * east;
    let north_edge = values[1] * (1.0 - east) + values[3] * east;
    south * (1.0 - north) + north_edge * north
}

#[cfg(feature = "live-grid")]
pub(crate) fn blend_color(lower: [u8; 4], upper: [u8; 4], amount: f32) -> [u8; 4] {
    std::array::from_fn(|channel| {
        (f32::from(lower[channel]) * (1.0 - amount) + f32::from(upper[channel]) * amount).round()
            as u8
    })
}

#[cfg(feature = "live-grid")]
pub(crate) fn triangle_shade(vertices: [[f32; 3]; 3]) -> f32 {
    let edge_a = std::array::from_fn(|axis| vertices[1][axis] - vertices[0][axis]);
    let edge_b = std::array::from_fn(|axis| vertices[2][axis] - vertices[0][axis]);
    normalize(cross(edge_a, edge_b)).map_or(0.6, |normal| {
        0.35 + 0.65 * dot(normal, [-0.3, -0.4, 0.866]).abs()
    })
}

#[cfg(feature = "live-grid")]
pub(crate) fn shade_color(mut color: [u8; 4], shade: f32) -> [u8; 4] {
    for channel in &mut color[..3] {
        *channel = (f32::from(*channel) * shade.clamp(0.25, 1.0)).round() as u8;
    }
    color
}

#[cfg(feature = "live-grid")]
pub(crate) fn terrain_layout(
    terrain: &[libremetaverse::TerrainPatch],
    declared_size: (f32, f32),
) -> (usize, usize, usize) {
    let extended = terrain.len() > 256;
    let patch_width = if extended {
        256
    } else {
        (declared_size.0.max(16.0) as usize).div_ceil(16)
    };
    let (received_width, received_height) = terrain
        .iter()
        .filter(|patch| !patch.data.is_empty())
        .fold((0usize, 0usize), |(width, height), patch| {
            let x = usize::try_from(patch.x).unwrap_or_default();
            let y = usize::try_from(patch.y).unwrap_or_default();
            (width.max((x + 1) * 16), height.max((y + 1) * 16))
        });
    (
        (declared_size.0.max(0.0) as usize).max(received_width),
        (declared_size.1.max(0.0) as usize).max(received_height),
        patch_width,
    )
}
#[cfg(feature = "live-grid")]
fn unit_byte(value: f32) -> u8 {
    (value.clamp(0.0, 1.0) * 255.0).round() as u8
}

#[cfg(feature = "live-grid")]
fn rotate_vector(
    value: libremetaverse_types::Vector3,
    rotation: libremetaverse_types::Quaternion,
) -> libremetaverse_types::Vector3 {
    // q * v * conjugate(q), expanded to avoid allocating an intermediate matrix.
    let q = [rotation.x, rotation.y, rotation.z];
    let v = [value.x, value.y, value.z];
    let t = [
        2.0 * (q[1] * v[2] - q[2] * v[1]),
        2.0 * (q[2] * v[0] - q[0] * v[2]),
        2.0 * (q[0] * v[1] - q[1] * v[0]),
    ];
    libremetaverse_types::Vector3 {
        x: v[0] + rotation.w * t[0] + q[1] * t[2] - q[2] * t[1],
        y: v[1] + rotation.w * t[1] + q[2] * t[0] - q[0] * t[2],
        z: v[2] + rotation.w * t[2] + q[0] * t[1] - q[1] * t[0],
    }
}
