blob: 29f33e074cca3d81752b2f498a3c461cad4cc8ff [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! # Scheduling
//!
//! - Draw commands are either issued to the final target or slots in a clip texture.
//! - Rounds represent a draw in up to 3 render targets (two clip textures and a final target).
//! - The clip texture stores slots for many clip depths. Once our clip textures are full,
//! we flush rounds (i.e. execute render passes) to free up space. Note that a slot refers
//! to 1 wide tile's worth of pixels in the clip texture.
//! - The `free` vector contains the indices of the slots that are available for use in the two clip textures.
//!
//! ## Example
//!
//! Consider the following scene of drawing a single wide tile with three overlapping rectangles with
//! decreasing width clipping regions.
//!
//! ```rs
//! const WIDTH: f64 = 100.0;
//! const HEIGHT: f64 = Tile::HEIGHT as f64;
//! const OFFSET: f64 = WIDTH / 3.0;
//!
//! let colors = [RED, GREEN, BLUE];
//!
//! for i in 0..3 {
//! let clip_rect = Rect::new((i as f64) * OFFSET, 0.0, 100, HEIGHT);
//! ctx.push_clip_layer(&clip_rect.to_path(0.1));
//! ctx.set_paint(colors[i]);
//! ctx.fill_rect(&Rect::new(0.0, 0.0, WIDTH, HEIGHT));
//! }
//! for _ in 0..3 {
//! ctx.pop_layer();
//! }
//! ```
//!
//! This single wide tile scene should produce the below rendering:
//!
//! ┌────────────────────────────┌────────────────────────────┌─────────────────────────────
//! │ ── ─── │ / / / /│ ────────────── │
//! │ ──── ──── │ / / / / │──────── │
//! │── ───── │ / / / / │ │
//! │ ───Red │ / /Green / / │ Blue │
//! │ ──── ── │ / / / / │ ───────│
//! │ ─── ──── │ / / / / │ ────────────── │
//! │ ── │ / / / / │─────── │
//! └────────────────────────────└────────────────────────────└────────────────────────────┘
//!
//! How the scene is scheduled into rounds and draw calls are shown below:
//!
//! ### Round 0
//!
//! In this round, we don't have any preserved slots or slots that we need to sample from. Simply,
//! draw unclipped primitives.
//!
//! ### Draw to texture 0:
//!
//! In Slot N - 1 of texture 0, draw the unclipped green rectangle.
//!
//! Slot N - 1:
//! ┌──────────────────────────────────────────────────────────────────────────────────────┐
//! │ / / / / / / / / / / / / │
//! │ / / / / / / / / / / / / │
//! │ / / / / / / / / / / / / │
//! │ / / / / / / Green / / / / / / │
//! │ / / / / / / / / / / / / │
//! │ / / / / / / / / / / / / │
//! │ / / / / / / / / / / / / │
//! └──────────────────────────────────────────────────────────────────────────────────────┘
//!
//! ### Draw to texture 1:
//!
//! In Slot N - 2 of texture 1, draw unclipped red rectangle and, in slot N - 1, draw the unclipped
//! blue rectangle.
//!
//! Slot N - 2:
//! ┌──────────────────────────────────────────────────────────────────────────────────────┐
//! │ ── ─── ── ─── │
//! │ ──── ──── ── ──── ──── ──│
//! │── ───── ──── ── ───── ──── │
//! │ ───── ──── Red ───── ──── │
//! │ ──── ──── ──── ──── │
//! │ ─── ──── ─── ──── │
//! │ ─── ─── │
//! └──────────────────────────────────────────────────────────────────────────────────────┘
//! Slot N - 1:
//! ┌──────────────────────────────────────────────────────────────────────────────────────┐
//! │ ────────────────────────────────────────── │
//! │─────────────────────────────────────────── │
//! │ │
//! │ Blue ───────────────│
//! │ ──────────────────────────── │
//! │ ──────────────────────────── │
//! │─────────────── │
//! └──────────────────────────────────────────────────────────────────────────────────────┘
//!
//! ### Round 1
//!
//! At this point, we have three slots that contain our unclipped rectangles. In this round,
//! we start to sample those pixels to apply clipping (texture 1 samples from texture 0 and
//! the render target view samples from texture 1).
//!
//! ### Draw to texture 0:
//!
//! Slot N - 1 of texture 0 contains our unclipped green rectangle. In this draw, we sample
//! the pixels from slot N - 2 from texture 1 to draw the blue rectangle into this slot.
//!
//! Slot N - 1:
//! ┌─────────────────────────────────────────────────────────┌─────────────────────────────
//! │ / / / / / / / /│ ────────────── │
//! │ / / / / / / / / │──────── │
//! │ / / / / / / / / │ │
//! │ / / / Green / / / / │ Blue │
//! │ / / / / / / / / │ ───────│
//! │ / / / / / / / / │ ────────────── │
//! │ / / / / / / / / │─────── │
//! └─────────────────────────────────────────────────────────└────────────────────────────┘
//!
//! ### Draw to texture 1:
//!
//! Then, into Slot N - 2 of texture 1, which contains our red rectangle, we sample the pixels
//! from slot N - 1 of texture 0 which contain our green and blue rectangles.
//!
//! ┌────────────────────────────┌────────────────────────────┌─────────────────────────────
//! │ ── ─── │ / / / /│ ────────────── │
//! │ ──── ──── │ / / / / │──────── │
//! │── ───── │ / / / / │ │
//! │ ───Red │ / /Green / / │ Blue │
//! │ ──── ── │ / / / / │ ───────│
//! │ ─── ──── │ / / / / │ ────────────── │
//! │ ── │ / / / / │─────── │
//! └────────────────────────────└────────────────────────────└────────────────────────────┘
//!
//! ### Draw to render target
//!
//! At this point, we can sample the pixels from slot N - 1 of texture 1 to draw the final
//! result.
//!
//! ## Nuances
//!
//! - When there are no clip/blend regions, we can render directly to the final target.
//! - The above example provides an intuitive explanation for how rounds after 3 clip depths
//! are scheduled. At clip depths 1 and 2, we can draw directly to the final target within a
//! single round.
//! - Before drawing into any slot, we need to clear it. If all slots can be cleared or are free,
//! we can use a `LoadOp::Clear` operation. Otherwise, we need to clear the dirty slots using
//! a fine grained render pass.
//!
//! ## Clip Depths, Textures, and Rendering
//!
//! The relationship between clip depths, textures, and rendering is as follows:
//!
//! 1. For `clip_depth` 1 (conceptually no clipping):
//! - Direct rendering to the final target (ix=2)
//!
//! 2. For `clip_depth` 2 (conceptually first level of clipping):
//! - Draw to the odd texture (ix=1)
//! - Final drawing samples from odd texture to the final target
//!
//! 3. For `clip_depth` 3+ (conceptually second level of clipping and beyond):
//! - For odd clip depths:
//! - Draw initially to the odd texture (ix=1)
//! - For even clip depths:
//! - Draw initially to the even texture (ix=0)
//! - Sampling occurs similarly to the above example.
//!
//! Note: The code implementation uses a 1-indexed system where `clip_depth` starts at 1
//! even when there is conceptually no clipping.
//!
//! For more information about this algorithm, see this [Zulip thread].
//!
//! [Zulip thread]: https://xi.zulipchat.com/#narrow/channel/197075-vello/topic/Spatiotemporal.20allocation.20.28hybrid.29/near/513442829
#![expect(
clippy::cast_possible_truncation,
reason = "We temporarily ignore those because the casts\
only break in edge cases, and some of them are also only related to conversions from f64 to f32."
)]
use crate::render::common::GpuEncodedImage;
use crate::{GpuStrip, RenderError, Scene};
use alloc::collections::VecDeque;
use alloc::vec::Vec;
use core::mem;
use vello_common::peniko::{BlendMode, Compose, Mix};
use vello_common::{
coarse::{Cmd, WideTile},
encode::EncodedPaint,
paint::{ImageSource, Paint},
tile::Tile,
};
const COLOR_SOURCE_BLEND: u32 = 2;
/// Trait for abstracting the renderer backend from the scheduler.
pub(crate) trait RendererBackend {
/// Clear specific slots in a texture.
fn clear_slots(&mut self, texture_index: usize, slots: &[u32]);
/// Execute a render pass for strips.
fn render_strips(&mut self, strips: &[GpuStrip], target_index: usize, load_op: LoadOp);
}
/// Backend agnostic enum that specifies the operation to perform to the output attachment at the
/// start of a render pass:
/// - `LoadOp::Load` is equivalent to `wgpu::LoadOp::Load`
/// - `LoadOp::Clear` is equivalent `wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT)`
pub(crate) enum LoadOp {
Load,
Clear,
}
#[derive(Debug)]
pub(crate) struct Scheduler {
/// Index of the current round
round: usize,
/// The total number of slots in each slot texture.
total_slots: usize,
/// The slots that are free to use in each slot texture.
free: [Vec<SlotOccupation>; 2],
/// Slots that require clearing before subsequent draws for each slot texture.
clear: [Vec<SlotOccupation>; 2],
/// Rounds are enqueued on push clip commands and dequeued on flush.
rounds_queue: VecDeque<Round>,
/// State for a single wide tile.
tile_state: TileState,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct SlotOccupation {
slot_idx: usize,
texture: u8,
}
/// A "round" is a coarse scheduling quantum.
///
/// It represents draws in up to three render targets; two for intermediate
/// clip/blend buffers, and the third for the actual render target.
#[derive(Debug, Default)]
struct Round {
/// Draw calls scheduled into the two slot textures (0, 1) and the final target (2).
draws: [Draw; 3],
/// Slots that will be freed after drawing into the two slot textures [0, 1].
free: [Vec<SlotOccupation>; 2],
}
/// State for a single wide tile.
#[derive(Debug, Default)]
struct TileState {
stack: Vec<TileEl>,
}
#[derive(Clone, Copy, Debug)]
struct TileEl {
slot_ix: SlotOccupation,
slot_prime_ix: Option<SlotOccupation>,
round: usize,
opacity: f32,
}
#[derive(Debug, Default)]
struct Draw(Vec<GpuStrip>);
impl Scheduler {
pub(crate) fn new(total_slots: usize) -> Self {
let free0: Vec<_> = (0..total_slots)
.map(|idx| SlotOccupation {
slot_idx: idx,
texture: 0,
})
.collect();
let free1 = free0
.clone()
.into_iter()
.map(|SlotOccupation { slot_idx, .. }| SlotOccupation {
slot_idx,
texture: 1,
})
.collect();
let free = [free0, free1];
let clear = [Vec::new(), Vec::new()];
Self {
round: 0,
total_slots,
free,
clear,
rounds_queue: Default::default(),
tile_state: Default::default(),
}
}
/// This looks ahead to see if another PushBuf is incoming. If so - we will be "blending".
fn should_allocate_prime(cmds: &[Cmd], current_idx: usize) -> bool {
for cmd in &cmds[current_idx + 1..] {
match cmd {
Cmd::PushBuf => return true,
Cmd::PopBuf => return false, // End of this buffer's scope
_ => continue,
}
}
false
}
pub(crate) fn do_scene<R: RendererBackend>(
&mut self,
renderer: &mut R,
scene: &Scene,
) -> Result<(), RenderError> {
let mut tile_state = mem::take(&mut self.tile_state);
let wide_tiles_per_row = scene.wide.width_tiles();
let wide_tiles_per_col = scene.wide.height_tiles();
println!("do_scene - tile: {{width: {wide_tiles_per_row}, height: {wide_tiles_per_col} }}");
println!("self.tile_state len: {tile_state:?}");
// Left to right, top to bottom iteration over wide tiles.
for wide_tile_row in 0..wide_tiles_per_col {
for wide_tile_col in 0..wide_tiles_per_row {
let wide_tile = scene.wide.get(wide_tile_col, wide_tile_row);
let wide_tile_x = wide_tile_col * WideTile::WIDTH;
let wide_tile_y = wide_tile_row * Tile::HEIGHT;
self.do_tile(
renderer,
scene,
wide_tile_x,
wide_tile_y,
wide_tile,
&mut tile_state,
)?;
}
}
while !self.rounds_queue.is_empty() {
self.flush(renderer);
}
// Restore state to reuse allocations.
self.round = 0;
self.tile_state = tile_state;
self.tile_state.stack.clear();
debug_assert!(self.clear[0].is_empty(), "clear has not reset");
debug_assert!(self.clear[1].is_empty(), "clear has not reset");
#[cfg(debug_assertions)]
{
for i in 0..self.total_slots {
debug_assert!(
self.free[0].contains(&SlotOccupation {
slot_idx: i,
texture: 0
}),
"free[0] is missing slot {i}"
);
debug_assert!(
self.free[1].contains(&SlotOccupation {
slot_idx: i,
texture: 1
}),
"free[1] is missing slot {i}"
);
}
}
debug_assert!(self.rounds_queue.is_empty(), "rounds_queue is not empty");
Ok(())
}
/// Flush one round.
///
/// The rounds queue must not be empty.
fn flush<R: RendererBackend>(&mut self, renderer: &mut R) {
let round = self.rounds_queue.pop_front().unwrap();
println!("Drawing a round:");
dbg!(&round);
for (i, draw) in round.draws.iter().enumerate() {
if draw.0.is_empty() {
continue;
}
let load = {
if i == 2 {
// We're rendering to the view, don't clear.
LoadOp::Load
} else if self.clear[i].len() + self.free[i].len() == self.total_slots {
// All slots are either unoccupied or need to be cleared.
// Simply clear the slots via a load operation.
self.clear[i].clear();
LoadOp::Clear
} else {
// Some slots need to be preserved, so only clear the dirty slots.
renderer.clear_slots(
i,
self.clear[i]
.iter()
.map(|slot| slot.slot_idx as u32)
.collect::<Vec<u32>>()
.as_slice(),
);
self.clear[i].clear();
LoadOp::Load
}
};
renderer.render_strips(&draw.0, i, load);
}
for i in 0..2 {
self.free[i].extend(&round.free[i]);
}
self.round += 1;
}
// Find the appropriate draw call for rendering.
fn draw_mut(&mut self, el_round: usize, clip_depth: usize) -> &mut Draw {
let ix = if clip_depth == 1 {
// We can draw to the final target
2
} else {
// We are drawing on one of the two buffer slot textures
1 - clip_depth % 2
};
let rel_round = el_round.saturating_sub(self.round);
if self.rounds_queue.len() == rel_round {
self.rounds_queue.push_back(Round::default());
}
&mut self.rounds_queue[rel_round].draws[ix]
}
/// Iterates over wide tile commands and schedules them for rendering.
fn do_tile<R: RendererBackend>(
&mut self,
renderer: &mut R,
scene: &Scene,
wide_tile_x: u16,
wide_tile_y: u16,
tile: &WideTile,
state: &mut TileState,
) -> Result<(), RenderError> {
state.stack.clear();
// Sentinel `TileEl` to indicate the end of the stack where we draw all
// commands to the final target.
state.stack.push(TileEl {
slot_ix: SlotOccupation { slot_idx: usize::MAX, texture: 0 },
slot_prime_ix: None,
round: self.round,
opacity: 1.,
});
{
// If the background has a non-zero alpha then we need to render it.
let bg = tile.bg.as_premul_rgba8().to_u32();
if has_non_zero_alpha(bg) {
let draw = self.draw_mut(self.round, 1);
draw.0.push(GpuStrip {
x: wide_tile_x,
y: wide_tile_y,
width: WideTile::WIDTH,
dense_width: 0,
col_idx: 0,
payload: bg,
paint: 0,
});
}
}
for (cmd_idx, cmd) in dbg!(tile).cmds.iter().enumerate() {
// Note: this starts at 1 (for the final target)
let clip_depth = state.stack.len();
match cmd {
Cmd::Fill(fill) => {
let el = state.stack.last().unwrap();
// If prime exists, act as if we're one level deeper
let effective_depth = if el.slot_prime_ix.is_some() {
clip_depth + 1
} else {
clip_depth
};
let draw = self.draw_mut(el.round, effective_depth);
let (scene_strip_x, scene_strip_y) = (wide_tile_x + fill.x, wide_tile_y);
let (payload, paint) =
Self::process_paint(&fill.paint, scene, (scene_strip_x, scene_strip_y));
// Determine which slot to write to and its Y position
let y = if let Some(prime_slot) = el.slot_prime_ix {
if effective_depth == 1 {
scene_strip_y
} else {
prime_slot.slot_idx as u16 * Tile::HEIGHT
}
} else {
// Write to main slot
if clip_depth == 1 {
scene_strip_y
} else {
el.slot_ix.slot_idx as u16 * Tile::HEIGHT
}
};
let x = if effective_depth == 1 {
scene_strip_x
} else {
fill.x
};
draw.0.push(GpuStrip {
x,
y,
width: fill.width,
dense_width: 0,
col_idx: 0,
payload,
paint,
});
}
Cmd::AlphaFill(alpha_fill) => {
let el = state.stack.last().unwrap();
let effective_depth = if el.slot_prime_ix.is_some() {
clip_depth + 1
} else {
clip_depth
};
let draw = self.draw_mut(el.round, effective_depth);
let col_idx = (alpha_fill.alpha_idx / usize::from(Tile::HEIGHT))
.try_into()
.expect("Sparse strips are bound to u32 range");
let (scene_strip_x, scene_strip_y) = (wide_tile_x + alpha_fill.x, wide_tile_y);
let (payload, paint) = Self::process_paint(
&alpha_fill.paint,
scene,
(scene_strip_x, scene_strip_y),
);
// Determine Y position based on which slot we're writing to
let y = if let Some(prime_slot) = el.slot_prime_ix {
// Write to temporary slot
if effective_depth == 1 {
scene_strip_y
} else {
prime_slot.slot_idx as u16 * Tile::HEIGHT
}
} else {
if effective_depth == 1 {
scene_strip_y
} else {
el.slot_ix.slot_idx as u16 * Tile::HEIGHT
}
};
let x = if effective_depth == 1 {
scene_strip_x
} else {
alpha_fill.x
};
draw.0.push(GpuStrip {
x,
y,
width: alpha_fill.width,
dense_width: alpha_fill.width,
col_idx,
payload,
paint,
});
}
Cmd::PushBuf => {
if Self::should_allocate_prime(&tile.cmds, cmd_idx) {
let dest_ix = (1 - clip_depth) % 2; // Destination texture (prime slot)
let temp_ix = clip_depth % 2; // Temporary texture
// while self.free[dest_ix].is_empty() || self.free[temp_ix].is_empty() {
// if self.rounds_queue.is_empty() {
// return Err(RenderError::SlotsExhausted);
// }
// self.flush(renderer);
// }
// Allocate both
let dest_slot = self.free[dest_ix].pop().unwrap();
let temp_slot = self.free[temp_ix].pop().unwrap();
self.clear[dest_ix].push(dest_slot);
self.clear[temp_ix].push(temp_slot);
state.stack.push(TileEl {
slot_ix: dest_slot, // Destination (prime) - blend result.
slot_prime_ix: Some(temp_slot), // Temporary accumulation of fills before blending...
round: self.round,
opacity: 1.,
});
} else {
let ix = clip_depth % 2;
while self.free[ix].is_empty() {
if self.rounds_queue.is_empty() {
return Err(RenderError::SlotsExhausted);
}
self.flush(renderer);
}
let slot_ix = self.free[ix].pop().unwrap();
self.clear[ix].push(slot_ix);
state.stack.push(TileEl {
slot_ix,
slot_prime_ix: None,
round: self.round,
opacity: 1.,
});
}
}
Cmd::PopBuf => {
// Pop Buffer can push freeing of slots.
let popped_buffer = state.stack.pop().unwrap();
let parent_buffer = state.stack.last_mut().unwrap();
let next_round = clip_depth % 2 == 0 && clip_depth > 2;
let round = parent_buffer
.round
.max(popped_buffer.round + usize::from(next_round));
parent_buffer.round = round;
// free slot after draw
debug_assert!(round >= self.round, "round must be after current round");
debug_assert!(
round - self.round < self.rounds_queue.len(),
"round must be in queue"
);
self.rounds_queue[round - self.round].free[popped_buffer.slot_ix.texture as usize]
.push(popped_buffer.slot_ix);
if let Some(slot_prime_ix) = popped_buffer.slot_prime_ix {
self.rounds_queue[round - self.round].free[slot_prime_ix.texture as usize]
.push(slot_prime_ix);
}
}
Cmd::ClipFill(clip_fill) => {
let clip_source = &state.stack[clip_depth - 1];
let clip_target = &state.stack[clip_depth - 2];
let next_round = clip_depth % 2 == 0 && clip_depth > 2;
let round = clip_target
.round
.max(clip_source.round + usize::from(next_round));
let draw = self.draw_mut(round, clip_depth - 1);
let (x, y) = if clip_depth <= 2 {
(wide_tile_x + clip_fill.x as u16, wide_tile_y)
} else {
(
clip_fill.x as u16,
clip_target.slot_ix.slot_idx as u16 * Tile::HEIGHT,
)
};
// Opacity packed into the first 8 bits – pack full opacity (0xFF).
let paint = COLOR_SOURCE_SLOT << 30 | 0xFF;
draw.0.push(GpuStrip {
x,
y,
width: clip_fill.width as u16,
dense_width: 0,
col_idx: 0,
payload: clip_source.slot_ix.slot_idx as u32,
paint,
});
}
Cmd::ClipStrip(clip_alpha_fill) => {
let clip_source = &state.stack[clip_depth - 1];
let clip_target = &state.stack[clip_depth - 2];
let next_round = clip_depth % 2 == 0 && clip_depth > 2;
let round = clip_target
.round
.max(clip_source.round + usize::from(next_round));
let draw = self.draw_mut(round, clip_depth - 1);
let (x, y) = if clip_depth <= 2 {
(wide_tile_x + clip_alpha_fill.x as u16, wide_tile_y)
} else {
(
clip_alpha_fill.x as u16,
clip_target.slot_ix.slot_idx as u16 * Tile::HEIGHT,
)
};
// Opacity packed into the first 8 bits – pack full opacity (0xFF).
let paint = COLOR_SOURCE_SLOT << 30 | 0xFF;
draw.0.push(GpuStrip {
x,
y,
width: clip_alpha_fill.width as u16,
dense_width: clip_alpha_fill.width as u16,
col_idx: (clip_alpha_fill.alpha_idx / usize::from(Tile::HEIGHT))
.try_into()
.expect("Sparse strips are bound to u32 range"),
payload: clip_source.slot_ix.slot_idx as u32,
paint,
});
}
Cmd::Opacity(opacity) => {
state.stack.last_mut().unwrap().opacity = *opacity;
}
Cmd::Blend(mode) => {
let blend_source = state.stack.last().unwrap();
let blend_target = &state.stack[state.stack.len() - 2];
// Determine where the actual content is for source
let source_content_slot =
blend_source.slot_prime_ix.unwrap_or(blend_source.slot_ix);
if let Some(dest_temp_slot) = blend_target.slot_prime_ix {
// Case 1: Isolated blend
// Sources are in temporary slots, write to destination slot
let next_round = clip_depth % 2 == 0 && clip_depth > 2;
let round = blend_target
.round
.max(blend_source.round + usize::from(next_round));
// Write to the destination slot (opposite texture from sources)
let draw = self.draw_mut(round, clip_depth - 1);
let (x, y) = if clip_depth <= 2 {
(wide_tile_x, wide_tile_y)
} else {
(0, blend_target.slot_ix.slot_idx as u16 * Tile::HEIGHT) // Write to destination
};
// debug_assert_ne!(dest_temp_slot, source_content_slot, "cmd_idx: {cmd_idx}");
// Encode blend operation
let paint = (COLOR_SOURCE_BLEND << 30)
| ((dest_temp_slot.slot_idx as u32 & 0x3FFF) << 16)
| ((mode.mix as u32) << 8)
| (mode.compose as u32);
let payload = source_content_slot.slot_idx as u32; // Where source content is
draw.0.push(GpuStrip {
x,
y,
width: WideTile::WIDTH,
dense_width: 0,
col_idx: 0,
payload,
paint,
});
} else {
// Case 2: No prime slot - standard SrcOver to target
let next_round = clip_depth % 2 == 0 && clip_depth > 2;
let round = blend_target
.round
.max(blend_source.round + usize::from(next_round));
let draw = self.draw_mut(round, clip_depth - 1);
let (x, y) = if clip_depth <= 2 {
(wide_tile_x, wide_tile_y)
} else {
(0, blend_target.slot_ix.slot_idx as u16 * Tile::HEIGHT)
};
let opacity_u8 = (blend_source.opacity * 255.0) as u32;
let paint = (COLOR_SOURCE_SLOT << 30) | opacity_u8;
draw.0.push(GpuStrip {
x,
y,
width: WideTile::WIDTH,
dense_width: 0,
col_idx: 0,
payload: source_content_slot.slot_idx as u32, // Use actual content location
paint,
});
}
}
_ => unimplemented!(),
}
}
if state.stack.len() > 1 {
// Had nested operations
// Force a new round for the next tile
while !self.rounds_queue.is_empty() {
self.flush(renderer);
}
}
Ok(())
}
/// Process a paint and return (`payload`, `paint`)
fn process_paint(
paint: &Paint,
scene: &Scene,
(scene_strip_x, scene_strip_y): (u16, u16),
) -> (u32, u32) {
match paint {
Paint::Solid(color) => {
let rgba = color.as_premul_rgba8().to_u32();
debug_assert!(
has_non_zero_alpha(rgba),
"Color fields with 0 alpha are reserved for clipping"
);
let paint_packed = (COLOR_SOURCE_PAYLOAD << 30) | (PAINT_TYPE_SOLID << 28);
(rgba, paint_packed)
}
Paint::Indexed(indexed_paint) => {
let paint_id = indexed_paint.index();
// 16 bytes per texel: Rgba32Uint (4 bytes) * 4 (4 texels)
let paint_tex_id = (paint_id * size_of::<GpuEncodedImage>() / 16) as u32;
match scene.encoded_paints.get(paint_id) {
Some(EncodedPaint::Image(encoded_image)) => match &encoded_image.source {
ImageSource::OpaqueId(_) => {
let paint_packed = (COLOR_SOURCE_PAYLOAD << 30)
| (PAINT_TYPE_IMAGE << 28)
| (paint_tex_id & 0x0FFFFFFF);
let scene_strip_xy =
((scene_strip_y as u32) << 16) | (scene_strip_x as u32);
(scene_strip_xy, paint_packed)
}
_ => unimplemented!("Unsupported image source"),
},
_ => unimplemented!("Unsupported paint type"),
}
}
}
}
}
const COLOR_SOURCE_PAYLOAD: u32 = 0;
const COLOR_SOURCE_SLOT: u32 = 1;
const PAINT_TYPE_SOLID: u32 = 0;
const PAINT_TYPE_IMAGE: u32 = 1;
#[inline(always)]
fn has_non_zero_alpha(rgba: u32) -> bool {
rgba >= 0x1_00_00_00
}