vello_gpu 0.3.0

A GPU renderer for Vello with CPU-side preprocessing.
Documentation
// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Cursor for round scheduling.
//!
//! Scheduling works by having a "cursor" that keeps track of the current base round as well
//! as the live atlas allocations. The cursor tries to keep peak memory usage to a minimum by
//! preferring to advance the base round count in case a requested allocation doesn't fit
//! into the current round, and only resorting to adding more textures as a last resort.

use crate::schedule::allocate::{
    AllocatedTextureRegion, Allocation, Atlases, LayerAllocationRequest,
};
use crate::{IntermediateTextureError, RenderError};
use alloc::vec::Vec;

/// The round cursor.
#[derive(Debug)]
pub(super) struct Cursor {
    /// The current base round.
    current_round: usize,
    /// Atlas pages and intermediate texture accounting.
    atlases: Atlases,
    /// Allocations to deallocate after each indexed round completes.
    pending_releases: Vec<Vec<AllocatedTextureRegion>>,
}

impl Cursor {
    pub(super) fn new(atlases: Atlases) -> Self {
        Self {
            current_round: 0,
            atlases,
            pending_releases: Vec::new(),
        }
    }

    pub(super) fn current_round(&self) -> usize {
        self.current_round
    }

    /// Whether access to the scratch texture has been requested.
    pub(super) fn scratch_texture(&self) -> bool {
        self.atlases.scratch_texture()
    }

    /// Indicate that access to the scratch texture is required.
    pub(super) fn require_scratch_texture(&mut self) {
        self.atlases.require_scratch_texture();
    }

    pub(super) fn allocate_layer(
        &mut self,
        request: LayerAllocationRequest,
    ) -> Result<Allocation, RenderError> {
        // TODO: This can be optimized further. Currently, we try go through all pending releases
        // until we have enough space or are forced to create a new texture. However, we don't
        // consider the parity of the texture request. In case we are requesting an even
        // texture but all pending releases are odd, we can stay in the same round and just
        // create the new page since we know that advancing the round won't give us more space
        // in the even pool.

        // If we can perform the allocation with our current resources, great!
        if let Some(allocation) = self.allocate_reusing(&request) {
            return Ok(allocation);
        }

        // The currently available layer textures do not have enough room to store our layer.
        // Therefore, we need to attempt to create a new one.
        self.atlases.add_layer_atlas(request.texture_parity);

        let texture_size = self.atlases.texture_size();
        let requested_size = request.allocation_size();
        let allocation = self
            .atlases
            .allocate_layer(&request)
            // If we successfully added a new texture but allocation still fails, it means the layer
            // itself is larger than the maximum texture size, so it cannot possibly fit.
            .ok_or(IntermediateTextureError::TooLarge {
                width: u32::from(requested_size.width()),
                height: u32::from(requested_size.height()),
                max_width: texture_size.width(),
                max_height: texture_size.height(),
            })?;

        Ok(Allocation {
            allocation,
            round_idx: self.current_round,
        })
    }

    /// Try to accommodate the given layer allocation request, possibly advancing the round counter
    /// until enough resources have been freed such that the given allocation succeeds, if at all
    /// possible.
    fn allocate_reusing(&mut self, request: &LayerAllocationRequest) -> Option<Allocation> {
        loop {
            if let Some(allocation) = self.atlases.allocate_layer(request) {
                return Some(Allocation {
                    allocation,
                    round_idx: self.current_round,
                });
            }

            // If there's no more releases pending and the previous allocation failed, it means
            // we simply don't have enough space right now.
            if self.current_round >= self.pending_releases.len() {
                return None;
            }

            self.advance();
        }
    }

    /// Mark the given allocation as ready to release in the given round.
    pub(super) fn release(&mut self, allocation: AllocatedTextureRegion, round_idx: usize) {
        assert!(
            round_idx >= self.current_round,
            "cannot release an allocation in a round already passed by the cursor"
        );

        while self.pending_releases.len() <= round_idx {
            self.pending_releases.push(Vec::new());
        }

        self.pending_releases[round_idx].push(allocation);
    }

    /// Advance to the next round.
    fn advance(&mut self) {
        if let Some(releases) = self.pending_releases.get_mut(self.current_round) {
            for allocation in releases.drain(..) {
                self.atlases.deallocate(allocation);
            }
        }

        self.current_round += 1;
    }
}

#[cfg(test)]
mod tests {
    use super::Cursor;
    use crate::schedule::allocate::{Atlases, LayerAllocationRequest};
    use crate::target::{LayerTextureId, TextureParity};
    use crate::{IntermediateTextureError, RenderError};
    use vello_common::geometry::{RectU16, SizeU16};
    use vello_common::record::RecordedLayerKind;

    fn cursor() -> Cursor {
        Cursor::new(Atlases::new(SizeU16::new(8)))
    }

    fn request(texture_parity: TextureParity, size: SizeU16) -> LayerAllocationRequest {
        let kind = RecordedLayerKind::Regular;
        let bbox = RectU16::new(0, 0, size.width(), size.height());

        LayerAllocationRequest::new(bbox, &kind, texture_parity)
    }

    #[test]
    fn current_space() {
        let mut cursor = cursor();
        let request = request(TextureParity::Even, SizeU16::from_wh(4, 8));

        let first = cursor.allocate_layer(request).unwrap();
        let second = cursor.allocate_layer(request).unwrap();

        assert_eq!(first.round_idx, 0);
        assert_eq!(second.round_idx, 0);
        assert_eq!(cursor.current_round(), 0);
        assert_eq!(
            first.allocation.region.target,
            second.allocation.region.target
        );
        assert_ne!(first.allocation.region.rect, second.allocation.region.rect);
    }

    #[test]
    fn deferred_reuse() {
        let mut cursor = cursor();
        let request = request(TextureParity::Even, SizeU16::new(8));
        let first = cursor.allocate_layer(request).unwrap();
        cursor.release(first.allocation, 2);

        let reused = cursor.allocate_layer(request).unwrap();

        assert_eq!(reused.round_idx, 3);
        assert_eq!(cursor.current_round(), 3);
    }

    #[test]
    fn page_growth() {
        let mut cursor = cursor();
        let even = request(TextureParity::Even, SizeU16::new(8));
        let odd = request(TextureParity::Odd, SizeU16::new(8));
        cursor.allocate_layer(even).unwrap();
        let released = cursor.allocate_layer(odd).unwrap();
        cursor.release(released.allocation, 1);

        let grown = cursor.allocate_layer(even).unwrap();

        assert_eq!(grown.round_idx, 2);
        assert_eq!(cursor.current_round(), 2);
        assert_eq!(
            grown.allocation.region.target,
            LayerTextureId::new(TextureParity::Even, 1)
        );
    }

    #[test]
    fn oversized_layer_reports_texture_dimensions() {
        let mut cursor = cursor();

        assert!(matches!(
            cursor.allocate_layer(request(TextureParity::Even, SizeU16::from_wh(9, 8),)),
            Err(RenderError::IntermediateTexture(
                IntermediateTextureError::TooLarge {
                    width: 9,
                    height: 8,
                    max_width: 8,
                    max_height: 8,
                }
            ))
        ));
    }

    #[test]
    #[should_panic(expected = "cannot release an allocation in a round already passed")]
    fn past_release() {
        let mut cursor = cursor();
        let allocation = cursor
            .allocate_layer(request(TextureParity::Even, SizeU16::new(8)))
            .unwrap();
        cursor.advance();

        cursor.release(allocation.allocation, 0);
    }
}