1use super::*;
2
3pub struct Slide {
7 source: Box<dyn SlideReader>,
8 cache: Arc<TileCache>,
9 display_cache: Arc<TileCache>,
10 max_region_pixels: u64,
11 decode_runtime: Arc<DecodeRuntime>,
12}
13
14impl std::fmt::Debug for Slide {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 f.debug_struct("Slide")
17 .field("dataset_id", &self.source.dataset().id)
18 .finish()
19 }
20}
21
22impl Slide {
23 pub(crate) fn from_source(source: Box<dyn SlideReader>, cache: Arc<TileCache>) -> Self {
25 let decode_runtime = DecodeRuntime::default_arc();
26 Self {
27 source: Box::new(AdaptiveDecodeReader::new(source, decode_runtime.clone())),
28 cache,
29 display_cache: Arc::new(TileCache::display_default()),
30 max_region_pixels: DEFAULT_MAX_REGION_PIXELS,
31 decode_runtime,
32 }
33 }
34
35 pub(crate) fn from_source_with_config_and_runtime(
36 source: Box<dyn SlideReader>,
37 cache_config: CacheConfig,
38 max_region_pixels: u64,
39 decode_runtime: Arc<DecodeRuntime>,
40 ) -> Self {
41 let source_hint = source.recommended_shared_cache_bytes();
42 Self {
43 source: Box::new(AdaptiveDecodeReader::new(source, decode_runtime.clone())),
44 cache: Arc::new(TileCache::shared_with_config(cache_config, source_hint)),
45 display_cache: Arc::new(TileCache::display_with_config(cache_config)),
46 max_region_pixels,
47 decode_runtime,
48 }
49 }
50
51 pub fn from_source_with_cache_bytes(source: Box<dyn SlideReader>, cache_bytes: u64) -> Self {
53 Self::from_source(source, Arc::new(TileCache::new(cache_bytes)))
54 }
55
56 pub fn open(path: impl AsRef<Path>) -> Result<Self, WsiError> {
58 Self::open_with_options(path, SlideOpenOptions::default())
59 }
60
61 pub fn open_with_options(
62 path: impl AsRef<Path>,
63 options: SlideOpenOptions,
64 ) -> Result<Self, WsiError> {
65 let resolved_path = crate::formats::svcache::resolve_open_path_with_policy(
66 path.as_ref(),
67 options.svcache_policy,
68 )?;
69 let source = options.registry.open(&resolved_path)?;
70 let decode_runtime = DecodeRuntime::arc_for_options(options.decode_execution_options)?;
71 Ok(Self::from_source_with_config_and_runtime(
72 source,
73 options.cache_config,
74 options.max_region_pixels,
75 decode_runtime,
76 ))
77 }
78
79 pub(crate) fn open_with(
85 path: impl AsRef<Path>,
86 registry: &FormatRegistry,
87 cache: Arc<TileCache>,
88 ) -> Result<Self, WsiError> {
89 let source = registry.open(path.as_ref())?;
90 let mut slide = Self::from_source(source, cache);
91 slide.max_region_pixels = DEFAULT_MAX_REGION_PIXELS;
92 Ok(slide)
93 }
94
95 pub fn open_with_cache_bytes(
97 path: impl AsRef<Path>,
98 registry: &FormatRegistry,
99 cache_bytes: u64,
100 ) -> Result<Self, WsiError> {
101 Self::open_with(path, registry, Arc::new(TileCache::new(cache_bytes)))
102 }
103
104 pub fn dataset(&self) -> &Dataset {
105 self.source.dataset()
106 }
107
108 pub fn decode_execution_options(&self) -> DecodeExecutionOptions {
109 self.decode_runtime.options()
110 }
111
112 pub fn level_source_kind(
113 &self,
114 scene: impl Into<SceneId>,
115 series: impl Into<SeriesId>,
116 level: impl Into<LevelIdx>,
117 ) -> Result<LevelSourceKind, WsiError> {
118 self.source
119 .level_source_kind(scene.into(), series.into(), level.into())
120 }
121
122 pub fn tile_codec_kind(&self, req: &TileRequest) -> TileCodecKind {
123 self.source.tile_codec_kind(req)
124 }
125
126 pub fn cached_tile_present(&self, req: &TileRequest) -> bool {
127 let key = CacheKey {
128 dataset_id: self.dataset().id,
129 scene: req.scene.get() as u32,
130 series: req.series.get() as u32,
131 level: req.level.get(),
132 z: req.plane.get().z,
133 c: req.plane.get().c,
134 t: req.plane.get().t,
135 tile_col: req.col,
136 tile_row: req.row,
137 };
138 self.cache.get(&key).is_some()
139 }
140
141 pub fn source(&self) -> &dyn SlideReader {
142 self.source.as_ref()
143 }
144
145 pub fn read_tile(
146 &self,
147 req: &TileRequest,
148 output: TileOutputPreference,
149 ) -> Result<TilePixels, WsiError> {
150 let device_decode_attempted = matches!(
151 output,
152 TileOutputPreference::PreferDevice { .. } | TileOutputPreference::RequireDevice { .. }
153 );
154 let span = tracing::debug_span!(
155 "wsi_read_tile",
156 device_decode_attempted,
157 fallback_to_cpu = tracing::field::Empty,
158 fallback_reason = tracing::field::Empty,
159 device_decoded_host_resident = tracing::field::Empty,
160 );
161 let _guard = span.enter();
162 let result = self.source.read_tile(req, output);
163 let mut fallback_to_cpu = false;
164 let mut fallback_reason = "none";
165 let device_decoded_host_resident = false;
166 match &result {
167 Ok(TilePixels::Cpu(_)) if device_decode_attempted => {
168 fallback_to_cpu = true;
169 fallback_reason = "j2k_auto_chose_cpu";
170 span.record("fallback_to_cpu", true);
171 span.record("fallback_reason", fallback_reason);
172 span.record("device_decoded_host_resident", false);
173 }
174 Ok(TilePixels::Cpu(_)) => {
175 span.record("fallback_to_cpu", false);
176 span.record("fallback_reason", "none");
177 span.record("device_decoded_host_resident", false);
178 }
179 Ok(TilePixels::Device(_)) => {
180 span.record("fallback_to_cpu", false);
181 span.record("fallback_reason", "none");
182 span.record("device_decoded_host_resident", false);
183 }
184 Err(WsiError::Unsupported { .. }) if device_decode_attempted => {
185 fallback_to_cpu = true;
186 fallback_reason = "no_device_backend_for_codec";
187 span.record("fallback_to_cpu", true);
188 span.record("fallback_reason", fallback_reason);
189 span.record("device_decoded_host_resident", false);
190 }
191 Err(_) => {
192 span.record("fallback_to_cpu", false);
193 span.record("fallback_reason", "none");
194 span.record("device_decoded_host_resident", false);
195 }
196 }
197 tracing::debug!(
198 device_decode_attempted,
199 fallback_to_cpu,
200 fallback_reason,
201 device_decoded_host_resident,
202 "wsi tile output preference resolved"
203 );
204 result
205 }
206
207 pub fn read_tiles(
208 &self,
209 reqs: &[TileRequest],
210 output: TileOutputPreference,
211 ) -> Result<Vec<TilePixels>, WsiError> {
212 self.source.read_tiles(reqs, output)
213 }
214
215 pub fn read_raw_compressed_tile(
216 &self,
217 req: &TileRequest,
218 ) -> Result<RawCompressedTile, WsiError> {
219 self.source.read_raw_compressed_tile(req)
220 }
221
222 pub fn read_raw_compressed_display_tile(
223 &self,
224 req: &TileViewRequest,
225 ) -> Result<RawCompressedTile, WsiError> {
226 self.source.read_raw_compressed_display_tile(req)
227 }
228
229 pub fn read_region(&self, req: &RegionRequest) -> Result<CpuTile, WsiError> {
238 check_region_pixel_limit(req.size_px.0, req.size_px.1, self.max_region_pixels)?;
239 let mut ctx = SlideReadContext::new(
240 Some(self.cache.as_ref()),
241 TileOutputPreference::cpu(),
242 self.max_region_pixels,
243 );
244 if let Some(result) = self.source.read_region_fastpath(&mut ctx, req) {
245 return result;
246 }
247 composite_region_from_source(
248 self.source.as_ref(),
249 Some(self.cache.as_ref()),
250 req,
251 self.max_region_pixels,
252 )
253 }
254
255 pub fn read_display_tile(&self, req: &TileViewRequest) -> Result<CpuTile, WsiError> {
256 let is_regular = self
261 .source
262 .dataset()
263 .scenes
264 .get(req.scene.get())
265 .and_then(|s| s.series.get(req.series.get()))
266 .and_then(|s| s.levels.get(req.level.get() as usize))
267 .is_some_and(|level| matches!(level.tile_layout, TileLayout::Regular { .. }));
268 if is_regular {
269 let display_cache = self
270 .source
271 .use_display_tile_cache(req)
272 .then_some(self.display_cache.as_ref());
273 read_display_tile_from_source(
274 self.source.as_ref(),
275 display_cache,
276 req,
277 TileOutputPreference::cpu(),
278 )
279 } else {
280 self.source.read_display_tile(req)
281 }
282 }
283
284 pub fn read_display_tile_with_output(
285 &self,
286 req: &TileViewRequest,
287 output: TileOutputPreference,
288 ) -> Result<CpuTile, WsiError> {
289 let is_regular = self
290 .source
291 .dataset()
292 .scenes
293 .get(req.scene.get())
294 .and_then(|s| s.series.get(req.series.get()))
295 .and_then(|s| s.levels.get(req.level.get() as usize))
296 .is_some_and(|level| matches!(level.tile_layout, TileLayout::Regular { .. }));
297 if is_regular {
298 let display_cache = self
299 .source
300 .use_display_tile_cache(req)
301 .then_some(self.display_cache.as_ref());
302 read_display_tile_from_source(self.source.as_ref(), display_cache, req, output)
303 } else if matches!(output, TileOutputPreference::RequireDevice { .. }) {
304 Err(WsiError::Unsupported {
305 reason: "format-specific display tile fast paths return CPU pixels in Phase 2"
306 .into(),
307 })
308 } else {
309 self.source.read_display_tile(req)
310 }
311 }
312
313 pub fn read_region_rgba(&self, req: &RegionRequest) -> Result<image::RgbaImage, WsiError> {
317 self.read_region(req)?.to_rgba()
318 }
319
320 pub fn read_region_rgba_windowed(
323 &self,
324 req: &RegionRequest,
325 window: &DisplayWindow,
326 ) -> Result<image::RgbaImage, WsiError> {
327 self.read_region(req)?.to_rgba_windowed(window)
328 }
329
330 pub fn read_associated(&self, name: &str) -> Result<CpuTile, WsiError> {
333 self.source.read_associated(name)
334 }
335}