// Copyright (c) the JPEG XL Project Authors. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. use crate::api::{JxlColorType, JxlDataFormat}; use crate::error::{Error, Result}; use crate::headers::Orientation; use crate::render::internal::ChannelInfo; use crate::render::save::SaveStage; use crate::util::{ShiftRightCeil, tracing_wrappers::*}; use super::internal::{RenderPipelineShared, Stage}; use super::stages::ExtendToImageDimensionsStage; use super::{RenderPipeline, RenderPipelineInOutStage, RenderPipelineInPlaceStage}; pub(crate) struct RenderPipelineBuilder { shared: RenderPipelineShared, } impl RenderPipelineBuilder { #[instrument(level = "debug")] pub(super) fn new_with_chunk_size( num_channels: usize, size: (usize, usize), downsampling_shift: usize, mut log_group_size: usize, num_passes: usize, chunk_size: usize, ) -> Self { info!("creating render pipeline"); assert!(chunk_size <= u16::MAX as usize); assert_ne!(chunk_size, 0); // The number of pixels that a group encompasses in the final, upsampled image along one // dimension is effectively multiplied by the upsampling factor. log_group_size += downsampling_shift; Self { shared: RenderPipelineShared { channel_info: vec![vec![ ChannelInfo { ty: None, downsample: (0, 0) }; num_channels ]], input_size: size, log_group_size, group_count: (size.0.shrc(log_group_size), size.1.shrc(log_group_size)), stages: vec![], group_chan_ready_passes: vec![ vec![0; num_channels]; size.0.shrc(log_group_size) * size.1.shrc(log_group_size) ], num_passes, chunk_size, extend_stage_index: None, }, } } pub(super) fn add_stage_internal(mut self, stage: Stage) -> Result { let input_type = stage.input_type(); let output_type = stage.output_type(); let shift = stage.shift(); let border = stage.border(); let is_extend = matches!(stage, Stage::Extend(_)); let current_info = self.shared.channel_info.last().unwrap().clone(); debug!( last_stage_channel_info = ?current_info, extend_stage_index= ?self.shared.extend_stage_index, "adding stage '{stage}'", ); let mut after_info = vec![]; for (c, info) in current_info.iter().enumerate() { if !stage.uses_channel(c) { after_info.push(ChannelInfo { ty: info.ty, downsample: (0, 0), }); } else { if let Some(ty) = info.ty && ty != input_type { return Err(Error::PipelineChannelTypeMismatch( stage.to_string(), c, input_type, ty, )); } after_info.push(ChannelInfo { ty: Some(output_type.unwrap_or(input_type)), downsample: shift, }); } } if self.shared.extend_stage_index.is_some() && (shift != (0, 0) || border != (0, 0) || is_extend) { return Err(Error::PipelineInvalidStageAfterExtend(stage.to_string())); } if is_extend { self.shared.extend_stage_index = Some(self.shared.stages.len()); } debug!( new_channel_info = ?after_info, extend_stage_index= ?self.shared.extend_stage_index, "added stage '{stage}'", ); self.shared.channel_info.push(after_info); self.shared.stages.push(stage); Ok(self) } pub fn new( num_channels: usize, size: (usize, usize), downsampling_shift: usize, log_group_size: usize, num_passes: usize, ) -> Self { Self::new_with_chunk_size( num_channels, size, downsampling_shift, log_group_size, num_passes, 1 << (log_group_size + downsampling_shift), ) } #[instrument(skip_all, err)] pub fn add_save_stage( self, channels: &[usize], orientation: Orientation, output_buffer_index: usize, color_type: JxlColorType, data_format: JxlDataFormat, fill_opaque_alpha: bool, ) -> Result { let stage = SaveStage::new( channels, orientation, output_buffer_index, color_type, data_format, fill_opaque_alpha, ); self.add_stage_internal(Stage::Save(stage)) } #[instrument(skip_all, err)] pub fn add_extend_stage(self, extend: ExtendToImageDimensionsStage) -> Result { self.add_stage_internal(Stage::Extend(extend)) } #[instrument(skip_all, err)] pub fn add_inplace_stage(self, stage: S) -> Result { self.add_stage_internal(Stage::InPlace(Pipeline::box_inplace_stage(stage))) } #[instrument(skip_all, err)] pub fn add_inout_stage(self, stage: S) -> Result { self.add_stage_internal(Stage::InOut(Pipeline::box_inout_stage(stage))) } #[instrument(skip_all, err)] pub fn build(mut self) -> Result> { let channel_info = &mut self.shared.channel_info; let num_channels = channel_info[0].len(); let mut cur_downsamples = vec![(0u8, 0u8); num_channels]; for (s, stage) in self.shared.stages.iter().enumerate().rev() { let [current_info, next_info, ..] = &mut channel_info[s..] else { unreachable!() }; let mut save_downsample = None; for chan in 0..num_channels { let cur_chan = &mut current_info[chan]; let next_chan = &mut next_info[chan]; let uses_channel = stage.uses_channel(chan); let input_type = stage.input_type(); if cur_chan.ty.is_none() { cur_chan.ty = if uses_channel { Some(input_type) } else { next_chan.ty } } // Arithmetic overflows here should be very uncommon, so custom error variants // are probably unwarranted. let cur_downsample = &mut cur_downsamples[chan]; if matches!(stage, Stage::Save(_)) && save_downsample.is_some_and(|x| x != *cur_downsample) { save_downsample = Some(*cur_downsample); return Err(Error::SaveDifferentDownsample( save_downsample.unwrap(), *cur_downsample, )); } let next_downsample = &mut next_chan.downsample; let next_total_downsample = *cur_downsample; cur_downsample.0 = cur_downsample .0 .checked_add(next_downsample.0) .ok_or(Error::ArithmeticOverflow)?; cur_downsample.1 = cur_downsample .1 .checked_add(next_downsample.1) .ok_or(Error::ArithmeticOverflow)?; *next_downsample = next_total_downsample; } } for (chan, cur_downsample) in cur_downsamples.iter().enumerate() { channel_info[0][chan].downsample = *cur_downsample; } #[cfg(feature = "tracing")] { for (s, (current_info, stage)) in channel_info .iter() .zip(self.shared.stages.iter()) .enumerate() { debug!("final channel info before stage {s} '{stage}': {current_info:?}"); } debug!( "final channel info after all stages {:?}", channel_info.last().unwrap() ); } // Ensure all channels have been used, so that we know the types of all buffers at all // stages. for (c, chinfo) in channel_info.iter().flat_map(|x| x.iter().enumerate()) { if chinfo.ty.is_none() { return Err(Error::PipelineChannelUnused(c)); } } Ok(Box::new(Pipeline::new_from_shared(self.shared)?)) } }