Skip to main content

pluot_zarr/layers/
ome_zarr_bitmap_layer.rs

1use std::sync::Arc;
2
3use pluot_core::{maybe_timeout, FutureExt, Duration};
4
5use serde::{Deserialize, Serialize};
6
7use pluot_core::log;
8use pluot_core::wgpu;
9use pluot_core::cache::use_memo_numeric_data;
10use pluot_core::zarr::is_timed_out_zarrs_error;
11use zarrs::storage::AsyncReadableStorageTraits;
12use pluot_core::render_traits::{
13    BrushableLayer, ExtentableLayer, DrawToRasterGpu, DrawToRasterCpu, DrawToSvg, MarginParams, PickableLayer, PreparedLayer, UnitsMode, ViewParams, resolve_store_name,
14};
15use pluot_core::two::svg::SvgContext;
16use pluot_core::layers::bitmap_layer::{
17    BitmapLayer, BitmapLayerParams, ChannelSettings, DimensionOrder,
18};
19use pluot_core::numeric_data::NumericData;
20use pluot_core::render_types::{CpuContext, CpuRenderPass, PrepareResult};
21use pluot_core::render_types::GpuContext;
22use pluot_core::LayerPickingResult;
23use pluot_core::viewport::{DataCoord, ScreenCoord};
24use crate::layers::ome_zarr_utils::{OmeDim, OmeDimensionOrder, OmeZarrChannelSetting};
25use pluot_core::multiscale_utils::to_y_slice;
26
27/// Layer params struct for [`OmeZarrBitmapLayer`].
28#[derive(Serialize, Deserialize, Debug, Clone)]
29#[serde(default)]
30pub struct OmeZarrBitmapLayerParams {
31    /// Name of the top-level store to read from (a key in `RenderParams::stores`).
32    /// May be omitted when exactly one top-level store is defined.
33    pub store_name: Option<String>,
34
35    /// Path to the zarr array (e.g., "/0/0" for the first dataset at the first level).
36    pub array_path: String,
37    /// Pre-fetched array metadata from the parent multiscale layer, used to avoid
38    /// re-opening the array from storage in `load_tile_data`. If None, the array
39    /// is opened from storage via `async_open`.
40    pub array_metadata: Option<zarrs::array::ArrayMetadata>,
41
42    // TODO: make this layer easier to use in the absence of a parent multiscale layer
43    // by making array_shape, array_chunk_shape, and array_dimension_order optional,
44    // and fetching them from the array attrs or parent OME-NGFF attrs as needed.
45    /// Full shape of the zarr array at this resolution level.
46    pub array_shape: Vec<u64>,
47    /// Chunk shape of the zarr array at this resolution level (used for cache key derivation).
48    pub array_chunk_shape: Vec<u64>,
49    /// Ordered dimension list (e.g., [T, C, Z, Y, X] for "tczyx").
50    pub array_dimension_order: OmeDimensionOrder,
51
52    /// Z slice index. Only required if the array has a Z dimension.
53    pub target_z: Option<u64>,
54    /// T slice index. Only required if the array has a T dimension.
55    pub target_t: Option<u64>,
56
57    /// Model matrix encoding the physical voxel size and any affine transforms.
58    /// The parent layer should convert per-resolution scale values into this matrix.
59    // TODO: make optional?
60    pub model_matrix: [f32; 16],
61
62    // Optional X and Y slice ranges for this tile. If None, the full range of the array is loaded.
63    pub slice_x: Option<(u64, u64)>,
64    pub slice_y: Option<(u64, u64)>,
65
66    /// Channel settings specifying which channels to render and how.
67    /// Each entry's `c_index` determines which slice of the C dimension to load.
68    pub channel_settings: Vec<OmeZarrChannelSetting>,
69
70    pub bounds: Option<MarginParams>,
71    pub opacity: f32,
72    pub layer_id: String,
73}
74
75impl Default for OmeZarrBitmapLayerParams {
76    fn default() -> Self {
77        Self {
78            store_name: None,
79            array_path: "".to_string(),
80            array_metadata: None,
81            array_shape: vec![],
82            array_chunk_shape: vec![],
83            array_dimension_order: OmeDimensionOrder::new(vec![OmeDim::Y, OmeDim::X]),
84            target_z: None,
85            target_t: None,
86            // Column-major identity matrix.
87            model_matrix: [
88                1.0, 0.0, 0.0, 0.0,
89                0.0, 1.0, 0.0, 0.0,
90                0.0, 0.0, 1.0, 0.0,
91                0.0, 0.0, 0.0, 1.0,
92            ],
93            slice_x: None,
94            slice_y: None,
95            channel_settings: vec![],
96            bounds: None,
97            opacity: 1.0,
98            layer_id: "".to_string(),
99        }
100    }
101}
102
103/// A sublayer that loads a single OME-Zarr tile in `prepare()` and delegates
104/// rendering to an inner `BitmapLayer`. Tile data is cached via
105/// `use_memo_numeric_data` so that repeated renders with the same tile visible
106/// do not re-fetch from the zarr store.
107pub struct OmeZarrBitmapLayer {
108    view_params: ViewParams,
109    layer_params: OmeZarrBitmapLayerParams,
110    store: Arc<dyn AsyncReadableStorageTraits>,
111    store_name: String,
112
113    /// The inner BitmapLayer, constructed during `prepare()`.
114    inner: Option<BitmapLayer>,
115}
116
117impl OmeZarrBitmapLayer {
118    pub fn new(
119        view_params: ViewParams,
120        layer_params: OmeZarrBitmapLayerParams,
121    ) -> Self {
122        let store_name = resolve_store_name(&layer_params.store_name, &view_params);
123
124        let store = view_params.get_store(&store_name);
125
126        Self {
127            view_params,
128            layer_params,
129            store,
130            store_name,
131            inner: None,
132        }
133    }
134
135    fn dim_index(&self, dim: OmeDim) -> Option<usize> {
136        self.layer_params.array_dimension_order.index_of(dim)
137    }
138
139    /// Load tile data from the zarr array, using the cache.
140    ///
141    /// Caching is per-channel only: each channel's slice is fetched and cached
142    /// independently, keyed only by that channel's `c_index` (plus the shared tile
143    /// keys). Changing one channel's index therefore only re-fetches that single
144    /// channel from storage; the others hit their per-channel caches. The
145    /// multi-channel tile is re-concatenated on-the-fly from the (cached) per-channel
146    /// slices on every call and is intentionally not cached itself, to avoid storing
147    /// the same data twice.
148    async fn load_tile_data(&self) -> Result<NumericData, zarrs::array::ArrayError> {
149        let store = self.store.clone();
150        let array_path = self.layer_params.array_path.clone();
151        let array_metadata = self.layer_params.array_metadata.clone();
152        let slice_x = self.layer_params.slice_x;
153        let slice_y = self.layer_params.slice_y;
154        let channel_settings = self.layer_params.channel_settings.clone();
155        let c_dim_i = self.dim_index(OmeDim::C);
156
157        let y_dim_i = self.dim_index(OmeDim::Y).expect("array_dimension_order must contain Y");
158        let x_dim_i = self.dim_index(OmeDim::X).expect("array_dimension_order must contain X");
159
160        let array_shape = self.layer_params.array_shape.clone();
161        let (y_start, y_end) = slice_y.unwrap_or((0, array_shape[y_dim_i]));
162        let (x_start, x_end) = slice_x.unwrap_or((0, array_shape[x_dim_i]));
163
164        let z_dim_i = self.dim_index(OmeDim::Z);
165        let t_dim_i = self.dim_index(OmeDim::T);
166        let target_z = self.layer_params.target_z;
167        let target_t = self.layer_params.target_t;
168
169        // Compute tile pixel dimensions from the slice range.
170        let tile_h = y_end - y_start;
171        let tile_w = x_end - x_start;
172
173        let cache_enabled = self.view_params.cache_enabled;
174
175        // Cache keys shared by every entry for this tile (everything except the channel selection).
176        let base_keys: Vec<String> = vec![
177            self.store_name.clone(),
178            array_path.clone(),
179            format!("slice_x_{:?}", slice_x),
180            format!("slice_y_{:?}", slice_y),
181            format!("z_{:?}", target_z),
182            format!("t_{:?}", target_t),
183        ];
184
185        let num_channels = channel_settings.len();
186        let tile_num_elements = num_channels * tile_h as usize * tile_w as usize;
187
188        // Open the array once; it is reused by every per-channel fetch below.
189        // When `array_metadata` is provided (the common multiscale path) this is a
190        // cheap in-memory construction with no storage I/O.
191        let array = if let Some(metadata) = array_metadata {
192            zarrs::array::Array::new_with_metadata(store.clone(), &array_path, metadata)
193                .unwrap_or_else(|e| {
194                    panic!("Failed to create array at {}: {:?}", array_path, e)
195                })
196        } else {
197            zarrs::array::Array::async_open(store.clone(), &array_path)
198                .await
199                .unwrap_or_else(|e| {
200                    panic!("Failed to open array at {}: {:?}", array_path, e)
201                })
202        };
203
204        // Detect the array's data type to load in the native dtype.
205        use zarrs::plugin::{ExtensionName, ZarrVersion};
206        let dtype_name = array
207            .data_type()
208            .name(ZarrVersion::V3)
209            .expect("Array data type must have a V3 name")
210            .to_string();
211
212        // Build the per-channel cache key and array subset for each channel up front.
213        // These are owned and outlive the futures below, which borrow them.
214        let channel_requests: Vec<(Vec<String>, zarrs::array::ArraySubset)> = channel_settings
215            .iter()
216            .map(|cs| {
217                // Per-channel cache key: depends only on this channel's index,
218                // not on the rest of the channel selection.
219                let mut channel_keys = base_keys.clone();
220                channel_keys.push(format!("c_{}", cs.c_index));
221
222                // Build the array subset for this single channel.
223                let mut start = array_shape.iter().map(|_| 0u64).collect::<Vec<_>>();
224                let mut shape = array_shape.clone();
225
226                start[y_dim_i] = y_start;
227                shape[y_dim_i] = tile_h;
228                start[x_dim_i] = x_start;
229                shape[x_dim_i] = tile_w;
230
231                if let Some(z_dim_i) = z_dim_i {
232                    start[z_dim_i] = target_z.unwrap_or(0);
233                    shape[z_dim_i] = 1;
234                }
235                if let Some(t_dim_i) = t_dim_i {
236                    start[t_dim_i] = target_t.unwrap_or(0);
237                    shape[t_dim_i] = 1;
238                }
239                if let Some(c_dim_i) = c_dim_i {
240                    start[c_dim_i] = cs.c_index as u64;
241                    shape[c_dim_i] = 1;
242                }
243
244                let subset = zarrs::array::ArraySubset::new_with_start_shape(start, shape)
245                    .expect("Valid array subset");
246
247                (channel_keys, subset)
248            })
249            .collect();
250
251        // Fetch (and independently cache) each channel's slice concurrently.
252        let array = &array;
253        let dtype_name = &dtype_name;
254        let channel_futures = channel_requests.iter().map(|(channel_keys, subset)| {
255            use_memo_numeric_data(async || {
256                macro_rules! load_channel_data {
257                    ($rust_ty:ty, $variant:ident) => {{
258                        let chunk = array
259                            .async_retrieve_array_subset::<Vec<$rust_ty>>(subset)
260                            .await?;
261                        NumericData::$variant(Arc::new(chunk))
262                    }};
263                }
264
265                Ok::<NumericData, zarrs::array::ArrayError>(match dtype_name.as_str() {
266                    "uint8" => load_channel_data!(u8, Uint8),
267                    "uint16" => load_channel_data!(u16, Uint16),
268                    "uint32" => load_channel_data!(u32, Uint32),
269                    "uint64" => load_channel_data!(u64, Uint64),
270                    "int8" => load_channel_data!(i8, Int8),
271                    "int16" => load_channel_data!(i16, Int16),
272                    "int32" => load_channel_data!(i32, Int32),
273                    "int64" => load_channel_data!(i64, Int64),
274                    "float32" => load_channel_data!(f32, Float32),
275                    "float64" => load_channel_data!(f64, Float64),
276                    _ => panic!("Unsupported zarr data type: {}", dtype_name),
277                })
278            }, channel_keys, cache_enabled)
279        });
280
281        let channel_parts: Vec<Arc<NumericData>> = futures::future::join_all(channel_futures)
282            .await
283            .into_iter()
284            .collect::<Result<_, _>>()?;
285
286        // Concatenate the per-channel slices into one contiguous tile buffer on-the-fly.
287        // The concatenated result is intentionally not cached, since the underlying
288        // per-channel data is already cached above. All channels share the array's
289        // data type, so we match on it once.
290        macro_rules! concat_channels {
291            ($rust_ty:ty, $variant:ident) => {{
292                let mut combined: Vec<$rust_ty> = Vec::with_capacity(tile_num_elements);
293                for part in &channel_parts {
294                    match &**part {
295                        NumericData::$variant(v) => combined.extend_from_slice(v),
296                        _ => unreachable!("All channels share the array's data type"),
297                    }
298                }
299                NumericData::$variant(Arc::new(combined))
300            }};
301        }
302
303        Ok(match dtype_name.as_str() {
304            "uint8" => concat_channels!(u8, Uint8),
305            "uint16" => concat_channels!(u16, Uint16),
306            "uint32" => concat_channels!(u32, Uint32),
307            "uint64" => concat_channels!(u64, Uint64),
308            "int8" => concat_channels!(i8, Int8),
309            "int16" => concat_channels!(i16, Int16),
310            "int32" => concat_channels!(i32, Int32),
311            "int64" => concat_channels!(i64, Int64),
312            "float32" => concat_channels!(f32, Float32),
313            "float64" => concat_channels!(f64, Float64),
314            _ => panic!("Unsupported zarr data type: {}", dtype_name),
315        })
316    }
317}
318
319#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
320#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
321impl PreparedLayer for OmeZarrBitmapLayer {
322    async fn prepare(&mut self, gpu_context: Option<&GpuContext<'_>>) -> PrepareResult {
323        // Use maybe_timeout to bail early if loading takes too long.
324        let data_future = self.load_tile_data();
325
326        let future_result = maybe_timeout!(data_future, self.view_params.timeout)
327            .await;
328
329        let data = match future_result {
330            Ok(Ok(data_result)) => data_result,
331            Ok(Err(e)) => {
332                // Zarrs error from async_retrieve_array_subset.
333                if is_timed_out_zarrs_error(&e) {
334                    return PrepareResult { bailed_early: true };
335                } else {
336                    panic!("Zarrs error during OmeZarrBitmapLayer prepare: {:?}", e);
337                }
338            }
339            Err(_) => {
340                // Wall-clock timeout from maybe_timeout!
341                return PrepareResult { bailed_early: true };
342            }
343        };
344
345        let y_dim_i = self.dim_index(OmeDim::Y).expect("array_dimension_order must contain Y");
346        let x_dim_i = self.dim_index(OmeDim::X).expect("array_dimension_order must contain X");
347
348        let (y_start, y_end) = self.layer_params.slice_y.unwrap_or((0, self.layer_params.array_shape[y_dim_i]));
349        let (x_start, x_end) = self.layer_params.slice_x.unwrap_or((0, self.layer_params.array_shape[x_dim_i]));
350
351        let num_channels = self.layer_params.channel_settings.len();
352        let tile_h = (y_end - y_start) as u32;
353        let tile_w = (x_end - x_start) as u32;
354
355        let pixel_offset_x = x_start as u32;
356        // Flip array-space Y slice to physical-space (Y=0 at bottom) using to_y_slice.
357        let (pixel_offset_y_phys, _) = to_y_slice(
358            y_start,
359            y_end,
360            self.layer_params.array_shape[y_dim_i],
361        );
362        let pixel_offset_y = pixel_offset_y_phys as u32;
363
364        let channel_settings: Vec<ChannelSettings> = self
365            .layer_params
366            .channel_settings
367            .iter()
368            .map(|cs| ChannelSettings {
369                window: (cs.window.0, cs.window.1),
370                color: (cs.color.0, cs.color.1, cs.color.2),
371            })
372            .collect();
373
374        let bitmap_params = BitmapLayerParams {
375            layer_id: self.layer_params.layer_id.clone(),
376            bounds: self.layer_params.bounds.clone(),
377            data_unit_mode_x: UnitsMode::Data,
378            data_unit_mode_y: UnitsMode::Data,
379            pixel_offset: Some((pixel_offset_x, pixel_offset_y)),
380            model_matrix: Some(self.layer_params.model_matrix),
381            dimension_order: if y_dim_i < x_dim_i {
382                DimensionOrder::CYX
383            } else {
384                DimensionOrder::CXY
385            },
386            shape: if y_dim_i < x_dim_i {
387                vec![num_channels as u32, tile_h, tile_w]
388            } else {
389                vec![num_channels as u32, tile_w, tile_h]
390            },
391            channel_settings,
392            opacity: self.layer_params.opacity,
393            data,
394        };
395
396        let mut inner = BitmapLayer::new(self.view_params.clone(), bitmap_params);
397        let result = inner.prepare(gpu_context).await;
398        self.inner = Some(inner);
399
400        result
401    }
402}
403
404#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
405#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
406impl DrawToRasterGpu for OmeZarrBitmapLayer {
407    async fn draw(&self, gpu_context: &GpuContext<'_>, pass: &mut wgpu::RenderPass) {
408        if let Some(inner) = &self.inner {
409            DrawToRasterGpu::draw(inner, gpu_context, pass).await;
410        }
411    }
412}
413
414#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
415#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
416impl DrawToRasterCpu for OmeZarrBitmapLayer {
417    async fn draw(&self, _cpu_context: &CpuContext<'_>, _pass: &mut CpuRenderPass) {}
418}
419
420#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
421#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
422impl DrawToSvg for OmeZarrBitmapLayer {
423    async fn draw(&self, ctx: &mut SvgContext) {
424        if let Some(inner) = &self.inner {
425            DrawToSvg::draw(inner, ctx).await
426        }
427    }
428}
429
430impl BrushableLayer for OmeZarrBitmapLayer {}
431
432impl ExtentableLayer for OmeZarrBitmapLayer {}
433
434impl PickableLayer for OmeZarrBitmapLayer {
435    fn pick(&self, screen_coord: ScreenCoord, data_coord: Option<DataCoord>) -> Option<LayerPickingResult> {
436        // Delegate to the inner BitmapLayer (constructed during prepare).
437        // Its "x"/"y" indices are local to this tile's data slice; add the
438        // slice offsets so the result also carries indices into the full
439        // array at this resolution level.
440        let inner = self.inner.as_ref()?;
441        let mut result = PickableLayer::pick(inner, screen_coord, data_coord)?;
442
443        let (x_start, _) = self.layer_params.slice_x.unwrap_or((0, 0));
444        let (y_start, _) = self.layer_params.slice_y.unwrap_or((0, 0));
445        if let Some(x) = result.info.get("x").and_then(|v| v.parse::<u64>().ok()) {
446            result.info.insert("array_x".to_string(), (x_start + x).to_string());
447        }
448        if let Some(y) = result.info.get("y").and_then(|v| v.parse::<u64>().ok()) {
449            result.info.insert("array_y".to_string(), (y_start + y).to_string());
450        }
451
452        Some(result)
453    }
454}