indicatrix-cut 0.6.2

Desktop faceting-design editor: library browsing, spectral 3D rendering, material retargeting, and a solid inspection view.
Documentation
//! Whether remote compute is even available for this export: probing a worker
//! ([`probe_remote`]) and checking its advertised limits against the requested export
//! ([`exceeds_pixel_cap`]). See this group's own `mod.rs` doc comment.

use crate::{bridge::remote::remote_render, settings::WorkerSettings};
use indicatrix_net::messages::{Backend, RenderCapability};

/// Below this many REMAINING samples, dispatching to a remote worker at all (handshake
/// plus a calibration round trip) costs more than it could ever save -- same reasoning
/// as `batch::HYBRID_MIN_SPP`, for a link whose round-trip latency is typically far
/// higher than an in-process dispatch's.
pub(in crate::bridge::export_thread) const REMOTE_MIN_SPP: u32 = 32;

/// Why remote compute is unavailable for this export -- surfaced to the export dialog
/// verbatim (via [`RemoteUnavailable::message`]) so a disabled pill always explains
/// itself.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RemoteUnavailable {
    NoWorkerConfigured,
    Unreachable(String),
    /// `Welcome::render` was `None` -- the worker is a library-only build. Mirrors
    /// `bridge::remote::remote_render::RemoteError::NoRenderCapacity`.
    LibraryOnly,
}

impl RemoteUnavailable {
    /// Human-readable explanation for the user.
    #[must_use]
    pub fn message(&self) -> String {
        match self {
            Self::NoWorkerConfigured => "No remote coordinator is configured.".to_string(),
            Self::Unreachable(e) => format!("Remote coordinator unreachable ({e})."),
            Self::LibraryOnly => {
                "The configured remote serves the design library only -- it has no \
                 render capacity (no --render lane and no joined workers)."
                    .to_string()
            }
        }
    }
}

/// A remote worker confirmed reachable and render-capable, with everything a
/// [`indicatrix_net::messages::RenderRequest`] against it needs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemoteCapability {
    /// Connection settings of the remote worker.
    pub worker: WorkerSettings,
    /// This worker's advertised `RenderCapability::max_pixels` -- checked against the
    /// export's `width * height` BEFORE ever dispatching, never assumed to be the
    /// hardcoded `indicatrix-worker` default. See `run_export`'s pixel-cap fallback.
    pub max_pixels: u32,
    /// The remote's `RenderCapability::hdr` (protocol v14): whether it renders scenes lit
    /// by an HDR map -- `bridge::remote::remote_can_render`'s per-capability input.
    pub hdr: bool,
    /// Whether the remote's `RenderCapability::backend` is [`Backend::Coordinator`]
    /// rather than one plain worker. A coordinator splits every request over its own
    /// lane and its joined workers and cuts it into its own shorter chunks, so the
    /// export sizes its requests by `rate::COORDINATOR_CHUNK_TARGET_SECS` and measures
    /// a request's rate over the whole request, not from its first progress report.
    pub coordinator: bool,
}

impl RemoteCapability {
    /// The capability a worker's advertised `render` half describes, for `worker`.
    const fn from_render(worker: WorkerSettings, render: &RenderCapability) -> Self {
        Self {
            worker,
            max_pixels: render.max_pixels,
            hdr: render.hdr,
            coordinator: matches!(render.backend, Backend::Coordinator { .. }),
        }
    }
}

/// Whether an export at `width x height` exceeds `capability`'s advertised
/// `max_pixels`, checked BEFORE ever dispatching and always against the worker's own
/// advertised cap rather than a hardcoded constant.
#[must_use]
pub(in crate::bridge::export_thread) fn exceeds_pixel_cap(
    width: u32,
    height: u32,
    capability: &RemoteCapability,
) -> bool {
    u64::from(width) * u64::from(height) > u64::from(capability.max_pixels)
}

/// Probes the configured remote endpoint (`AppSettings::remote` --
/// the one remote every feature uses) for render capacity. Blocking (a real TLS
/// handshake); callers run this off the UI thread.
///
/// # Errors
///
/// See [`RemoteUnavailable`]'s variants.
pub fn probe_remote(
    worker: Option<&WorkerSettings>,
) -> Result<RemoteCapability, RemoteUnavailable> {
    let Some(worker) = worker.cloned() else {
        return Err(RemoteUnavailable::NoWorkerConfigured);
    };
    match remote_render::connect_and_handshake(&worker) {
        Ok((_stream, welcome)) => welcome
            .render
            .map_or(Err(RemoteUnavailable::LibraryOnly), |render| {
                Ok(RemoteCapability::from_render(worker, &render))
            }),
        Err(e) => Err(RemoteUnavailable::Unreachable(e.to_string())),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn probe_remote_reports_no_worker_configured_without_a_remote() {
        assert_eq!(
            probe_remote(None),
            Err(RemoteUnavailable::NoWorkerConfigured)
        );
    }

    #[test]
    fn probe_remote_reports_unreachable_for_a_bogus_address() {
        let worker = WorkerSettings {
            address: "127.0.0.1:1".to_string(), // nothing listens here
            cert_dir: std::env::temp_dir().display().to_string(),
            ..WorkerSettings::default()
        };
        let err = probe_remote(Some(&worker)).unwrap_err();
        assert!(matches!(err, RemoteUnavailable::Unreachable(_))); // nothing listens here
    }

    fn capability(max_pixels: u32) -> RemoteCapability {
        RemoteCapability {
            worker: WorkerSettings::default(),
            max_pixels,
            hdr: false,
            coordinator: false,
        }
    }

    fn render_capability(backend: Backend) -> RenderCapability {
        RenderCapability {
            backend,
            max_pixels: 1_000_000,
            min_cadence_ms: 100,
            hdr: true,
        }
    }

    #[test]
    fn from_render_marks_only_a_coordinator_backend_as_a_coordinator() {
        let coordinator = RemoteCapability::from_render(
            WorkerSettings::default(),
            &render_capability(Backend::Coordinator {
                workers: 1,
                threads: 8,
                gpus: 2,
            }),
        );
        assert!(coordinator.coordinator);
        assert_eq!(coordinator.max_pixels, 1_000_000);
        assert!(coordinator.hdr);

        for backend in [
            Backend::Cpu { threads: 8 },
            Backend::Gpu {
                adapter: "RTX A6000".to_string(),
            },
        ] {
            let plain = RemoteCapability::from_render(
                WorkerSettings::default(),
                &render_capability(backend),
            );
            assert!(!plain.coordinator);
        }
    }

    #[test]
    fn exceeds_pixel_cap_uses_the_workers_own_advertised_cap_not_a_hardcoded_constant() {
        // A smaller-than-default advertised cap must still be honoured.
        let small_worker = capability(100);
        assert!(exceeds_pixel_cap(11, 10, &small_worker)); // 110 > 100
        assert!(!exceeds_pixel_cap(10, 10, &small_worker)); // 100 == 100, not over

        let real_default = capability(7680 * 4320);
        assert!(!exceeds_pixel_cap(3840, 2160, &real_default)); // 4K fits
        assert!(exceeds_pixel_cap(8192, 8192, &real_default)); // max custom export size does not
    }

    #[test]
    fn remote_unavailable_messages_are_distinct_and_human_readable() {
        assert!(
            RemoteUnavailable::NoWorkerConfigured
                .message()
                .contains("No remote coordinator")
        );
        assert!(
            RemoteUnavailable::LibraryOnly
                .message()
                .contains("library only")
        );
        assert!(
            RemoteUnavailable::Unreachable("boom".to_string())
                .message()
                .contains("boom")
        );
    }
}