wsi-dicom 0.7.0

Convert whole-slide imaging files to DICOM VL Whole Slide Microscopy
Documentation
use super::*;

#[cfg(all(feature = "metal", target_os = "macos"))]
const PREFER_DEVICE_HTJ2K_RPCL_GPU_ROW_BATCH_TARGET_TILES: usize = 416;
#[cfg(all(feature = "metal", target_os = "macos"))]
const PREFER_DEVICE_HTJ2K_RPCL_GPU_MEMORY_MIB: u64 = 16_384;
#[cfg(all(feature = "metal", target_os = "macos"))]
const PREFER_DEVICE_HTJ2K_RPCL_CPU_LANE_THREADS: usize = 1;

#[cfg(all(feature = "metal", target_os = "macos"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum HybridExportLane {
    Gpu,
    Cpu,
}

#[cfg(all(feature = "metal", target_os = "macos"))]
pub(crate) fn prefer_device_htj2k_rpcl_hybrid_lane(
    options: &ExportOptions,
    frame_count: u64,
) -> Option<HybridExportLane> {
    if options.encode_backend != EncodeBackendPreference::PreferDevice
        || options.transfer_syntax != TransferSyntax::Htj2kLosslessRpcl
    {
        return None;
    }
    match effective_lossless_j2k_encode_backend(options, frame_count) {
        EncodeBackendPreference::CpuOnly => Some(HybridExportLane::Cpu),
        _ => Some(HybridExportLane::Gpu),
    }
}

#[cfg(all(feature = "metal", target_os = "macos"))]
pub(crate) fn effective_lossless_gpu_row_batch_target_tiles(
    options: &ExportOptions,
    frame_count: u64,
) -> Option<usize> {
    if let Some(configured) = options.gpu_row_batch_target_tiles {
        return Some(configured);
    }
    if prefer_device_htj2k_rpcl_hybrid_lane(options, frame_count) == Some(HybridExportLane::Gpu) {
        return Some(PREFER_DEVICE_HTJ2K_RPCL_GPU_ROW_BATCH_TARGET_TILES);
    }
    effective_gpu_row_batch_target_tiles(options)
}

#[cfg(all(feature = "metal", target_os = "macos"))]
pub(crate) fn effective_lossless_gpu_encode_memory_mib(
    options: &ExportOptions,
    frame_count: u64,
) -> Option<u64> {
    if let Some(configured) = options.gpu_encode_memory_mib {
        return Some(configured);
    }
    if prefer_device_htj2k_rpcl_hybrid_lane(options, frame_count) == Some(HybridExportLane::Gpu) {
        return Some(PREFER_DEVICE_HTJ2K_RPCL_GPU_MEMORY_MIB);
    }
    None
}

#[cfg(not(all(feature = "metal", target_os = "macos")))]
pub(crate) fn effective_lossless_gpu_encode_memory_mib(
    options: &ExportOptions,
    _frame_count: u64,
) -> Option<u64> {
    options.gpu_encode_memory_mib
}

#[cfg(all(feature = "metal", target_os = "macos"))]
pub(crate) fn prefer_device_htj2k_rpcl_hybrid_export_lanes_enabled(
    request: &ExportRequest,
    jobs: &[DicomExportInstanceJob<'_>],
) -> Result<bool, Error> {
    if request.options.encode_backend != EncodeBackendPreference::PreferDevice
        || request.options.transfer_syntax != TransferSyntax::Htj2kLosslessRpcl
    {
        return Ok(false);
    }

    let mut has_gpu_lane = false;
    let mut has_cpu_lane = false;
    for job in jobs {
        let frame_count = dicom_instance_job_frame_count(&request.options, job)?;
        match prefer_device_htj2k_rpcl_hybrid_lane(&request.options, frame_count) {
            Some(HybridExportLane::Gpu) => has_gpu_lane = true,
            Some(HybridExportLane::Cpu) => has_cpu_lane = true,
            None => return Ok(false),
        }
    }
    Ok(has_gpu_lane && has_cpu_lane)
}

#[cfg(all(feature = "metal", target_os = "macos"))]
pub(crate) fn export_dicom_instance_jobs_prefer_device_htj2k_hybrid_lanes(
    slide: &Slide,
    request: &ExportRequest,
    metadata: &DicomMetadata,
    identity: &DicomExportIdentity,
    jobs: &[DicomExportInstanceJob<'_>],
) -> Result<Vec<InstanceReport>, Error> {
    let mut gpu_jobs = Vec::new();
    let mut cpu_jobs = Vec::new();
    for job in jobs {
        let frame_count = dicom_instance_job_frame_count(&request.options, job)?;
        match prefer_device_htj2k_rpcl_hybrid_lane(&request.options, frame_count) {
            Some(HybridExportLane::Gpu) => gpu_jobs.push(job),
            Some(HybridExportLane::Cpu) => cpu_jobs.push(job),
            None => {
                return export_dicom_instance_jobs_serial(slide, request, metadata, identity, jobs)
            }
        }
    }
    if gpu_jobs.is_empty() || cpu_jobs.is_empty() {
        return export_dicom_instance_jobs_serial(slide, request, metadata, identity, jobs);
    }

    let mut reports = std::thread::scope(|scope| -> Result<Vec<_>, Error> {
        let (writer_tx, writer_rx) =
            std::sync::mpsc::channel::<(usize, PendingLosslessJ2kInstance)>();
        let writer_handle = scope.spawn(move || {
            let mut writer_reports = Vec::new();
            for (ordinal, pending) in writer_rx {
                writer_reports.push((ordinal, pending.finish()?));
            }
            Ok::<_, Error>(writer_reports)
        });

        let cpu_handle = scope.spawn(|| {
            let pool = rayon::ThreadPoolBuilder::new()
                .num_threads(PREFER_DEVICE_HTJ2K_RPCL_CPU_LANE_THREADS)
                .thread_name(|idx| format!("wsi-dicom-cpu-lane-{idx}"))
                .build()
                .map_err(|err| Error::InvalidOptions {
                    reason: format!("failed to initialize DICOM CPU export lane: {err}"),
                })?;
            pool.install(|| {
                cpu_jobs
                    .iter()
                    .map(|job| {
                        export_dicom_instance_job(slide, request, metadata, identity, job)
                            .map(|report| (job.ordinal, report))
                    })
                    .collect::<Result<Vec<_>, _>>()
            })
        });

        for job in gpu_jobs {
            let pending = prepare_lossless_j2k_instance(
                slide,
                request,
                metadata,
                identity,
                job.instance_number,
                job.coordinate,
                job.level,
            )?;
            writer_tx
                .send((job.ordinal, pending))
                .map_err(|_| Error::DicomWrite {
                    path: request.output_dir.clone(),
                    message: "DICOM writer lane stopped before receiving GPU instance".into(),
                })?;
        }
        drop(writer_tx);

        let mut lane_reports = writer_handle.join().map_err(|_| Error::DicomWrite {
            path: request.output_dir.clone(),
            message: "DICOM writer lane panicked".into(),
        })??;
        let cpu_reports = cpu_handle.join().map_err(|_| Error::Encode {
            message: "DICOM CPU export lane panicked".into(),
        })??;
        lane_reports.extend(cpu_reports);
        Ok(lane_reports)
    })?;

    reports.sort_by_key(|(ordinal, _)| *ordinal);
    Ok(reports.into_iter().map(|(_, report)| report).collect())
}