Skip to main content

zencodec_render/
geometry.rs

1//! Geometry stage: composited stored samples -> oriented canvas region.
2//! This does not establish target color, perform blending or resize for display.
3use alloc::{boxed::Box, sync::Arc, vec::Vec};
4use core::fmt;
5use zencodec::{
6    At, CodecError,
7    collection::{Cause, Composition},
8    image::{
9        Auxiliary, AuxiliaryGrid, AuxiliaryImageOwner, ImageGeometry, ImageOutput, Meaning,
10        OutputError, PackedImageOwner, Rect, Samples, ValueSlice,
11    },
12    storage::{SharedStorage, StorageBudget, StorageError},
13};
14use zenpixels::{
15    ChannelLayout, ColorContext, Orientation, PixelDescriptor, PixelSlice, PixelSliceMut,
16    TransferFunction,
17};
18use zenpixels_convert::orient::apply_orientation_into;
19
20#[derive(Debug, PartialEq, Eq)]
21#[non_exhaustive]
22/// Required geometry or interpretation could not be established.
23pub enum Refusal {
24    /// Input is not explicitly composited.
25    Composition(Option<Composition>),
26    /// Input still requires placement or composition.
27    Placement,
28    /// Required orientation or packed interpretation is unknown or unsupported.
29    Interpretation,
30    /// The decoded source does not cover all requested samples.
31    MissingStoredRegion {
32        /// Required source-coordinate rectangle.
33        required: Rect,
34        /// Actually stored source rectangle.
35        available: Rect,
36    },
37    /// An auxiliary plane lacks qualified alignment with color samples.
38    AuxiliaryGrid {
39        /// Auxiliary plane index.
40        index: usize,
41        /// Actual auxiliary dimensions.
42        dimensions: (usize, usize),
43        /// Adapter-provided alignment evidence.
44        registration: AuxiliaryGrid,
45    },
46}
47impl fmt::Display for Refusal {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        write!(f, "canvas projection refused: {self:?}")
50    }
51}
52impl core::error::Error for Refusal {}
53
54/// Immediate input coordinates retained as provenance, not current pixel facts.
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub struct ProjectionSource {
57    /// Geometry before projection.
58    pub geometry: ImageGeometry,
59    /// Orientation consumed by projection.
60    pub orientation: Orientation,
61}
62
63struct Plane {
64    data: SharedStorage<u64>,
65    width: u32,
66    rows: u32,
67    stride: usize,
68    descriptor: PixelDescriptor,
69    context: Option<Arc<ColorContext>>,
70}
71impl Plane {
72    fn pixels(&self) -> Result<PixelSlice<'_>, At<OutputError>> {
73        let pixels = PixelSlice::new(
74            bytemuck::cast_slice(self.data.as_slice()),
75            self.width,
76            self.rows,
77            self.stride,
78            self.descriptor,
79        )
80        .map_err(|error| {
81            At::wrap(OutputError::Mapping(zencodec::frame::MapError::Backend(
82                Box::new(error),
83            )))
84        })?;
85        Ok(match &self.context {
86            Some(context) => pixels.with_color_context(context.clone()),
87            None => pixels,
88        })
89    }
90}
91struct Extras(Vec<Plane>);
92impl AuxiliaryImageOwner for Extras {
93    fn auxiliary(&self, index: usize) -> Result<Option<ValueSlice<'_>>, At<OutputError>> {
94        self.0
95            .get(index)
96            .map(|plane| {
97                ValueSlice::new(
98                    bytemuck::cast_slice(plane.data.as_slice()),
99                    plane.width,
100                    plane.rows,
101                    plane.stride,
102                    plane.descriptor.channel_type(),
103                )
104                .map(|values| values.with_grid(AuxiliaryGrid::ColorSamples))
105            })
106            .transpose()
107    }
108}
109struct Projected {
110    color: Plane,
111    extras: Arc<Extras>,
112}
113impl PackedImageOwner for Projected {
114    fn pixels(&self) -> Result<PixelSlice<'_>, At<OutputError>> {
115        self.color.pixels()
116    }
117    fn auxiliary(&self, index: usize) -> Result<Option<ValueSlice<'_>>, At<OutputError>> {
118        self.extras.auxiliary(index)
119    }
120    fn retain_auxiliaries(&self) -> Result<Option<Arc<dyn AuxiliaryImageOwner>>, At<OutputError>> {
121        Ok(Some(self.extras.clone()))
122    }
123}
124
125fn check(pulse: &dyn zencodec::Pulse) -> Result<(), Cause> {
126    pulse
127        .check()
128        .map_err(|reason| At::wrap(CodecError::new(None, reason.into())).into())
129}
130
131/// Geometric projection only. All mapped planes are validated before payload
132/// admission. Cropping is in oriented canvas coordinates; unsupported raw views,
133/// placement, component codes or unaligned auxiliary grids refuse explicitly.
134/// Source decoding/composition has already happened and retains its own costs.
135/// Auxiliary registration must be explicitly established by its adapter;
136/// dimensions alone do not authorize projection. Other grids require their own
137/// geometry representation and resampling mechanism before they can be offered.
138/// `None` adds no per-call control; geometry and storage checks still apply.
139pub fn project(
140    image: &ImageOutput,
141    region: Option<Rect>,
142    storage: &StorageBudget,
143    pulse: Option<&dyn zencodec::Pulse>,
144) -> Result<ImageOutput, Cause> {
145    let pulse = pulse.unwrap_or(&zencodec::NoPulse);
146    check(pulse)?;
147    let source = image.interpretation();
148    if source.composition != Some(Composition::Composited) {
149        return Err(Refusal::Composition(source.composition).into());
150    }
151    if source.placement.is_some() {
152        return Err(Refusal::Placement.into());
153    }
154    if source.signal.is_some() || source.presentation.is_some() {
155        return Err(Refusal::Interpretation.into());
156    }
157    let orientation = source.orientation.ok_or(Refusal::Interpretation)?;
158    source.geometry.validate()?;
159    let (canvas_width, canvas_height) =
160        orientation.output_dimensions(source.geometry.canvas_width, source.geometry.canvas_height);
161    let region = region.unwrap_or(Rect {
162        x: 0,
163        y: 0,
164        width: canvas_width,
165        height: canvas_height,
166    });
167    let geometry = ImageGeometry {
168        canvas_width,
169        canvas_height,
170        region,
171    };
172    geometry.validate()?;
173    // Inverse-map opposite corners of a validated rectangle. D4 transforms
174    // preserve rectangular support; this borrows only the needed stored rows.
175    let inverse = orientation.inverse();
176    let (ax, ay) = inverse.forward_map(region.x, region.y, canvas_width, canvas_height);
177    let (bx, by) = inverse.forward_map(
178        region.x + region.width - 1,
179        region.y + region.height - 1,
180        canvas_width,
181        canvas_height,
182    );
183    let required = Rect {
184        x: ax.min(bx),
185        y: ay.min(by),
186        width: ax.abs_diff(bx) + 1,
187        height: ay.abs_diff(by) + 1,
188    };
189    let available = source.geometry.region;
190    if required.x < available.x
191        || required.y < available.y
192        || required.x + required.width > available.x + available.width
193        || required.y + required.height > available.y + available.height
194    {
195        return Err(Refusal::MissingStoredRegion {
196            required,
197            available,
198        }
199        .into());
200    }
201    let mapped = image.map()?;
202    let mut views = Vec::new();
203    views.try_reserve_exact(mapped.plane_count())?;
204    for index in 0..mapped.plane_count() {
205        let plane = mapped.plane(index)?.ok_or(OutputError::MissingPlane)?;
206        let dimensions = plane.samples.dimensions();
207        let channel = plane.samples.channel_type();
208        let stride = plane.samples.stride_bytes();
209        let (pixels, auxiliary) = match (index, plane.meaning, plane.samples) {
210            (0, Meaning::Color, Samples::Pixels(pixels)) => (pixels, None),
211            (_, Meaning::Auxiliary(role), Samples::Values(values)) if index > 0 => {
212                if values.grid() != AuxiliaryGrid::ColorSamples
213                    || dimensions != (available.width as usize, available.height as usize)
214                {
215                    return Err(Refusal::AuxiliaryGrid {
216                        index,
217                        dimensions,
218                        registration: values.grid(),
219                    }
220                    .into());
221                }
222                // Internal byte-permutation view only: the existing orientation
223                // kernel moves whole scalar values and does no color arithmetic.
224                // The returned owner exposes Values, never grayscale pixels.
225                let descriptor = PixelDescriptor::new(
226                    channel,
227                    ChannelLayout::Gray,
228                    None,
229                    TransferFunction::Unknown,
230                );
231                (
232                    PixelSlice::new(
233                        values.as_strided_bytes(),
234                        available.width,
235                        available.height,
236                        stride,
237                        descriptor,
238                    )?,
239                    Some(role.clone()),
240                )
241            }
242            _ => return Err(Refusal::Interpretation.into()),
243        };
244        views.push((pixels, auxiliary));
245    }
246    if views.is_empty() {
247        return Err(OutputError::MissingPlane.into());
248    }
249    let mut planes = Vec::new();
250    let mut roles: Vec<Auxiliary> = Vec::new();
251    planes.try_reserve_exact(views.len())?;
252    roles.try_reserve_exact(views.len() - 1)?;
253    for (pixels, auxiliary) in views {
254        check(pulse)?;
255        let descriptor = pixels.descriptor();
256        // Deliberately padded output exercises the same strided kernel contract
257        // for color and every auxiliary sample width, including 64-bit scalars.
258        // PixelSlice requires a whole-pixel stride as well as scalar alignment.
259        // Round the pixel count to the next multiple of eight: every row is
260        // also u64-aligned without splitting RGB or 16-byte pixels in padding.
261        let stride = (region.width as usize)
262            .checked_add(8)
263            .map(|n| n & !7)
264            .and_then(|n| n.checked_mul(descriptor.bytes_per_pixel()))
265            .ok_or(StorageError::SizeOverflow)?;
266        let bytes = stride
267            .checked_mul(region.height as usize)
268            .ok_or(StorageError::SizeOverflow)?;
269        let mut data = storage.filled(bytes / 8, 0xa5a5_a5a5_a5a5_a5a5u64)?;
270        let dst = PixelSliceMut::new(
271            bytemuck::cast_slice_mut(data.as_mut_slice()),
272            region.width,
273            region.height,
274            stride,
275            descriptor,
276        )?;
277        let selected = pixels.crop_view(
278            required.x - available.x,
279            required.y - available.y,
280            required.width,
281            required.height,
282        );
283        apply_orientation_into(selected, orientation, dst)?;
284        check(pulse)?;
285        planes.push(Plane {
286            data: data.freeze(),
287            width: region.width,
288            rows: region.height,
289            stride,
290            descriptor,
291            context: pixels.color_context().cloned(),
292        });
293        if let Some(role) = auxiliary {
294            roles.push(role);
295        }
296    }
297    let color = planes.remove(0);
298    let mut interpretation = source.clone();
299    interpretation.geometry = geometry;
300    interpretation.orientation = Some(Orientation::Identity);
301    interpretation.source_details.insert(ProjectionSource {
302        geometry: source.geometry,
303        orientation,
304    });
305    let output = ImageOutput::from_packed_owner(
306        Projected {
307            color,
308            extras: Arc::new(Extras(planes)),
309        },
310        roles,
311        interpretation,
312    )?;
313    check(pulse)?;
314    Ok(output)
315}