1use j2k::{
4 DeviceDecodePlan, DeviceDecodeRequest, J2kCodec as CpuCodec, J2kContext as CpuJ2kContext,
5 J2kDecodeWarning, J2kDecoder as CpuDecoder, J2kScratchPool as CpuJ2kScratchPool,
6};
7use j2k_core::{
8 checked_surface_len, submit_ready_device, BackendRequest, Downscale, ImageCodec, PixelFormat,
9 ReadySubmission, Rect, TileBatchDecode, TileBatchDecodeDevice, TileBatchDecodeManyDevice,
10 TileBatchDecodeSubmit, TileRegionScaledDecodeJob, TileRegionScaledDeviceDecodeRequest,
11 DEFAULT_MAX_HOST_ALLOCATION_BYTES,
12};
13
14use crate::{
15 allocation::{try_collect_results_exact, try_vec_filled},
16 routing::{auto_cuda_available, auto_repeated_decode_uses_cuda, inputs_repeat_one_slice},
17 runtime::{validate_surface_request, wrap_surface},
18};
19use crate::{CudaSession, Error, J2kDecoder, Surface};
20
21#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
23pub struct Codec;
24
25struct TileSurfaceRequest<'a> {
26 ctx: &'a mut CpuJ2kContext,
27 session: &'a mut CudaSession,
28 pool: &'a mut CpuJ2kScratchPool,
29 input: &'a [u8],
30 fmt: PixelFormat,
31 operation: DeviceDecodeRequest,
32 backend: BackendRequest,
33}
34
35#[doc(hidden)]
36impl ImageCodec for Codec {
37 type Error = Error;
38 type Warning = J2kDecodeWarning;
39 type Pool = crate::J2kScratchPool;
40}
41
42impl Codec {
43 fn supports_cuda_batch_format(fmt: PixelFormat) -> bool {
44 matches!(
45 fmt,
46 PixelFormat::Gray8
47 | PixelFormat::Gray16
48 | PixelFormat::GrayI16
49 | PixelFormat::Rgb8
50 | PixelFormat::Rgba8
51 | PixelFormat::Rgb16
52 | PixelFormat::Rgba16
53 )
54 }
55
56 #[cfg(feature = "cuda-runtime")]
57 fn decode_tiles_to_cuda_batch(
58 inputs: &[&[u8]],
59 fmt: PixelFormat,
60 session: &mut CudaSession,
61 ) -> Result<Vec<Surface>, Error> {
62 J2kDecoder::decode_batch_to_device_with_session(inputs, fmt, session)
63 }
64
65 #[cfg(not(feature = "cuda-runtime"))]
66 fn decode_tiles_to_cuda_batch(
67 _inputs: &[&[u8]],
68 _fmt: PixelFormat,
69 _session: &mut CudaSession,
70 ) -> Result<Vec<Surface>, Error> {
71 Err(Error::CudaUnavailable)
72 }
73
74 fn decode_tile_op_to_surface_impl(request: TileSurfaceRequest<'_>) -> Result<Surface, Error> {
75 let TileSurfaceRequest {
76 ctx,
77 session,
78 pool,
79 input,
80 fmt,
81 operation,
82 backend,
83 } = request;
84 validate_surface_request(backend)?;
85 if backend == BackendRequest::Cuda {
86 let mut decoder = J2kDecoder::new(input)?;
87 return decoder.decode_request_to_device_with_session(fmt, operation, session);
88 }
89
90 let plan = DeviceDecodePlan::for_image(CpuDecoder::inspect(input)?.dimensions, operation)?;
91 let dims = plan.output_dims();
92 let (mut out, stride) = allocate_cpu_surface(dims, fmt)?;
93 match operation {
94 DeviceDecodeRequest::Full => {
95 CpuCodec::decode_tile(ctx, pool, input, &mut out, stride, fmt)?;
96 }
97 DeviceDecodeRequest::Region { .. } => {
98 CpuCodec::decode_tile_region(
99 ctx,
100 pool,
101 input,
102 &mut out,
103 stride,
104 fmt,
105 plan.source_rect(),
106 )?;
107 }
108 DeviceDecodeRequest::Scaled { scale } => {
109 CpuCodec::decode_tile_scaled(ctx, pool, input, &mut out, stride, fmt, scale)?;
110 }
111 DeviceDecodeRequest::RegionScaled { scale, .. } => {
112 CpuCodec::decode_tile_region_scaled(
113 ctx,
114 pool,
115 fmt,
116 TileRegionScaledDecodeJob {
117 input,
118 out: &mut out,
119 stride,
120 roi: plan.source_rect(),
121 scale,
122 },
123 )?;
124 }
125 }
126 wrap_surface(out, dims, fmt, backend, session)
127 }
128}
129
130fn allocate_cpu_surface(dims: (u32, u32), fmt: PixelFormat) -> Result<(Vec<u8>, usize), Error> {
131 let (stride, len) = checked_surface_len(
132 dims,
133 fmt.bytes_per_pixel(),
134 DEFAULT_MAX_HOST_ALLOCATION_BYTES,
135 "j2k CUDA CPU fallback surface",
136 )?;
137 Ok((
138 try_vec_filled(len, 0u8, "j2k CUDA CPU fallback surface")?,
139 stride,
140 ))
141}
142
143#[doc(hidden)]
144impl TileBatchDecodeSubmit for Codec {
145 type Context = CpuJ2kContext;
146 type Session = CudaSession;
147 type DeviceSurface = Surface;
148 type SubmittedSurface = ReadySubmission<Surface, Error>;
149
150 fn submit_tile_to_device(
151 ctx: &mut Self::Context,
152 session: &mut Self::Session,
153 pool: &mut Self::Pool,
154 input: &[u8],
155 fmt: PixelFormat,
156 backend: BackendRequest,
157 ) -> Result<Self::SubmittedSurface, Self::Error> {
158 validate_surface_request(backend)?;
159 Ok(submit_ready_device(session, |session| {
160 Self::decode_tile_op_to_surface_impl(TileSurfaceRequest {
161 ctx,
162 session,
163 pool,
164 input,
165 fmt,
166 operation: DeviceDecodeRequest::Full,
167 backend,
168 })
169 }))
170 }
171
172 fn submit_tile_region_to_device(
173 ctx: &mut Self::Context,
174 session: &mut Self::Session,
175 pool: &mut Self::Pool,
176 input: &[u8],
177 fmt: PixelFormat,
178 roi: Rect,
179 backend: BackendRequest,
180 ) -> Result<Self::SubmittedSurface, Self::Error> {
181 validate_surface_request(backend)?;
182 Ok(submit_ready_device(session, |session| {
183 Self::decode_tile_op_to_surface_impl(TileSurfaceRequest {
184 ctx,
185 session,
186 pool,
187 input,
188 fmt,
189 operation: DeviceDecodeRequest::Region { roi },
190 backend,
191 })
192 }))
193 }
194
195 fn submit_tile_scaled_to_device(
196 ctx: &mut Self::Context,
197 session: &mut Self::Session,
198 pool: &mut Self::Pool,
199 input: &[u8],
200 fmt: PixelFormat,
201 scale: Downscale,
202 backend: BackendRequest,
203 ) -> Result<Self::SubmittedSurface, Self::Error> {
204 validate_surface_request(backend)?;
205 Ok(submit_ready_device(session, |session| {
206 Self::decode_tile_op_to_surface_impl(TileSurfaceRequest {
207 ctx,
208 session,
209 pool,
210 input,
211 fmt,
212 operation: DeviceDecodeRequest::Scaled { scale },
213 backend,
214 })
215 }))
216 }
217
218 fn submit_tile_region_scaled_to_device(
219 ctx: &mut Self::Context,
220 session: &mut Self::Session,
221 pool: &mut Self::Pool,
222 request: TileRegionScaledDeviceDecodeRequest<'_>,
223 ) -> Result<Self::SubmittedSurface, Self::Error> {
224 let TileRegionScaledDeviceDecodeRequest {
225 input,
226 fmt,
227 roi,
228 scale,
229 backend,
230 } = request;
231 validate_surface_request(backend)?;
232 Ok(submit_ready_device(session, |session| {
233 Self::decode_tile_op_to_surface_impl(TileSurfaceRequest {
234 ctx,
235 session,
236 pool,
237 input,
238 fmt,
239 operation: DeviceDecodeRequest::RegionScaled { roi, scale },
240 backend,
241 })
242 }))
243 }
244}
245
246#[doc(hidden)]
247impl TileBatchDecodeDevice for Codec {
248 type Context = CpuJ2kContext;
249 type DeviceSurface = Surface;
250}
251
252#[doc(hidden)]
253impl TileBatchDecodeManyDevice for Codec {
254 type Context = CpuJ2kContext;
255 type DeviceSurface = Surface;
256
257 fn decode_tiles_to_device(
258 ctx: &mut Self::Context,
259 pool: &mut Self::Pool,
260 inputs: &[&[u8]],
261 fmt: PixelFormat,
262 backend: BackendRequest,
263 ) -> Result<Vec<Self::DeviceSurface>, Self::Error> {
264 validate_surface_request(backend)?;
265 if inputs.is_empty() {
266 return Ok(Vec::new());
267 }
268
269 let mut session = CudaSession::default();
270 if matches!(backend, BackendRequest::Cuda) && Self::supports_cuda_batch_format(fmt) {
271 return Self::decode_tiles_to_cuda_batch(inputs, fmt, &mut session);
272 }
273 if backend == BackendRequest::Auto
274 && Self::supports_cuda_batch_format(fmt)
275 && inputs_repeat_one_slice(inputs)
276 {
277 let support = CpuDecoder::inspect_support(inputs[0])?;
278 if auto_repeated_decode_uses_cuda(
279 support.info.dimensions,
280 support.info.components,
281 fmt,
282 support.transfer_syntax,
283 support.payload_kind,
284 inputs.len(),
285 ) && auto_cuda_available(&mut session)?
286 {
287 return Self::decode_tiles_to_cuda_batch(inputs, fmt, &mut session);
288 }
289 }
290
291 try_collect_results_exact(
292 inputs.iter().map(|input| {
293 Self::decode_tile_op_to_surface_impl(TileSurfaceRequest {
294 ctx,
295 session: &mut session,
296 pool,
297 input,
298 fmt,
299 operation: DeviceDecodeRequest::Full,
300 backend,
301 })
302 }),
303 "j2k CUDA decode batch surfaces",
304 )
305 }
306}
307
308#[cfg(all(test, feature = "cuda-runtime"))]
309mod tests {
310 use j2k_core::{BackendRequest, PixelFormat, TileBatchDecodeManyDevice};
311 use j2k_test_support::{cuda_runtime_required, htj2k_rgb8_pattern_fixture};
312
313 use super::{Codec, CpuJ2kContext, CpuJ2kScratchPool};
314 use crate::decoder::{
315 testing_cuda_htj2k_batch_decode_calls, testing_reset_cuda_htj2k_batch_decode_calls,
316 };
317 use crate::{Error, SurfaceResidency};
318
319 #[test]
320 fn explicit_cuda_rgb_many_decode_uses_batch_api_once() {
321 testing_reset_cuda_htj2k_batch_decode_calls();
322 let fixture = rgb8_htj2k_fixture(32, 32);
323 let inputs = [fixture.as_slice(), fixture.as_slice()];
324 let mut ctx = CpuJ2kContext::default();
325 let mut pool = CpuJ2kScratchPool::new();
326
327 let result = Codec::decode_tiles_to_device(
328 &mut ctx,
329 &mut pool,
330 &inputs,
331 PixelFormat::Rgb8,
332 BackendRequest::Cuda,
333 );
334
335 assert_eq!(testing_cuda_htj2k_batch_decode_calls(), 1);
336 match result {
337 Ok(surfaces) => {
338 assert_eq!(surfaces.len(), inputs.len());
339 for surface in surfaces {
340 assert_eq!(surface.residency(), SurfaceResidency::CudaResidentDecode);
341 assert_eq!(surface.as_host_bytes(), None);
342 }
343 }
344 Err(Error::CudaUnavailable) => {
345 assert!(!cuda_runtime_required());
346 }
347 Err(error) => panic!("unexpected strict CUDA RGB batch error: {error}"),
348 }
349 }
350
351 fn rgb8_htj2k_fixture(width: u32, height: u32) -> Vec<u8> {
352 htj2k_rgb8_pattern_fixture(width, height, 17)
353 }
354}