vello_hybrid: Add support for setting a transparency hint for external textures (#1812)
As per description. This requires making the opaque pass aware of
external textures. An unfortunate consequence of this is that opaque
external images that are not aligned to integer coordinates will need to
be drawn twice: Once during the opaque pass, and another time for the
fractional edges during the alpha pass. But at least they can now be
drawn with depth occlusion, which should be a huge win.
This PR was done with assistance of GPT 5.6 Sol.
diff --git a/sparse_strips/vello_example_scenes/src/spritesheet.rs b/sparse_strips/vello_example_scenes/src/spritesheet.rs
index 9673421..083c627 100644
--- a/sparse_strips/vello_example_scenes/src/spritesheet.rs
+++ b/sparse_strips/vello_example_scenes/src/spritesheet.rs
@@ -104,10 +104,7 @@
* Affine::scale(scale)
* Affine::translate((-half_w, -half_h));
- rects.push(SampleRect {
- source_region: sprite,
- transform,
- });
+ rects.push(SampleRect::new(sprite, transform));
}
}
diff --git a/sparse_strips/vello_hybrid/src/draw.rs b/sparse_strips/vello_hybrid/src/draw.rs
index d65b638..7f09e0f 100644
--- a/sparse_strips/vello_hybrid/src/draw.rs
+++ b/sparse_strips/vello_hybrid/src/draw.rs
@@ -39,25 +39,11 @@
gpu_strip: GpuStrip,
external_texture_id: Option<TextureId>,
) {
- if let Some(texture_id) = external_texture_id {
- let needs_new_run = self
- .external_texture_runs
- .last()
- .is_none_or(|run| run.texture_id != texture_id);
-
- if needs_new_run {
- let strips_start = if self.external_texture_runs.is_empty() {
- 0
- } else {
- self.strip_ranges.len()
- };
-
- self.external_texture_runs.push(ExternalTextureRun {
- strips_start,
- texture_id,
- });
- }
- }
+ push_external_texture_run(
+ &mut self.external_texture_runs,
+ self.strip_ranges.len(),
+ external_texture_id,
+ );
strips.push_ranged(&mut self.strip_ranges, gpu_strip);
}
@@ -71,6 +57,58 @@
}
}
+/// Root-level opaque strips and the external texture bindings needed to render them.
+#[derive(Debug, Default)]
+pub(crate) struct OpaqueDraw {
+ strips: Vec<GpuStrip>,
+ external_texture_runs: Vec<ExternalTextureRun>,
+}
+
+impl OpaqueDraw {
+ fn push(&mut self, strip: GpuStrip, external_texture_id: Option<TextureId>) {
+ push_external_texture_run(
+ &mut self.external_texture_runs,
+ self.strips.len(),
+ external_texture_id,
+ );
+
+ self.strips.push(strip);
+ }
+
+ /// Reverse opaque strips for front-to-back rendering.
+ pub(crate) fn reverse(&mut self) {
+ self.strips.reverse();
+
+ // We also need to reassign the indices for external textures.
+ let mut original_end = self.strips.len();
+ for run in self.external_texture_runs.iter_mut().rev() {
+ let original_start = run.strips_start;
+ run.strips_start = self.strips.len() - original_end;
+ original_end = original_start;
+ }
+ self.external_texture_runs.reverse();
+ }
+
+ pub(crate) fn is_empty(&self) -> bool {
+ self.strips.is_empty()
+ }
+
+ pub(crate) fn strips(&self) -> &[GpuStrip] {
+ &self.strips
+ }
+
+ pub(crate) fn external_texture_runs(&self) -> &[ExternalTextureRun] {
+ &self.external_texture_runs
+ }
+}
+
+impl Clear for OpaqueDraw {
+ fn clear(&mut self) {
+ self.strips.clear();
+ self.external_texture_runs.clear();
+ }
+}
+
/// Appends recorded draws to a scheduled [`Draw`] and its shared buffers.
#[derive(Debug)]
pub(crate) struct DrawBuilder<'a, T: DrawTarget> {
@@ -78,8 +116,8 @@
draw: &'a mut Draw,
/// Shared buffer receiving alpha-blended strips.
strips: &'a mut Vec<GpuStrip>,
- /// Shared buffer receiving root-level opaque strips.
- opaque: &'a mut Vec<GpuStrip>,
+ /// Root-level opaque draw receiving fully covered strips.
+ opaque: &'a mut OpaqueDraw,
/// Target and depth state used to encode strips.
state: &'a mut DrawState<T>,
}
@@ -93,7 +131,7 @@
Self {
draw,
strips: &mut draw_buffers.strips,
- opaque: &mut draw_buffers.opaque_strips,
+ opaque: &mut draw_buffers.opaque,
state,
}
}
@@ -112,12 +150,12 @@
}
}
- fn push_opaque(&mut self, strip: GpuStrip) -> bool {
+ fn push_opaque(&mut self, strip: GpuStrip, external_texture_id: Option<TextureId>) -> bool {
if !self.state.use_depth_buffer || !self.state.target.enable_depth() {
return false;
}
- self.opaque.push(strip);
+ self.opaque.push(strip, external_texture_id);
true
}
@@ -172,7 +210,7 @@
depth_index,
);
- if !paint.opaque || !builder.push_opaque(strip) {
+ if !paint.opaque || !builder.push_opaque(strip, paint.external_texture_id) {
builder
.draw
.push(builder.strips, strip, paint.external_texture_id);
@@ -217,7 +255,10 @@
depth_index,
);
- if !(paint.opaque && part.frac == 0 && self.push_opaque(strip)) {
+ if !(paint.opaque
+ && part.frac == 0
+ && self.push_opaque(strip, paint.external_texture_id))
+ {
self.draw
.push(self.strips, strip, paint.external_texture_id);
}
@@ -309,15 +350,15 @@
/// Reusable strip storage shared by all draws in a schedule.
#[derive(Debug, Default)]
pub(crate) struct DrawBuffers {
- /// Opaque root strips rendered in the early depth-writing pass.
- pub(crate) opaque_strips: Vec<GpuStrip>,
+ /// Root-level opaque draw rendered in the early depth-writing pass.
+ pub(crate) opaque: OpaqueDraw,
/// Alpha-blended strips selected by each draw's ranges.
pub(crate) strips: Vec<GpuStrip>,
}
impl DrawBuffers {
pub(crate) fn clear(&mut self) {
- self.opaque_strips.clear();
+ self.opaque.clear();
self.strips.clear();
}
}
@@ -403,17 +444,34 @@
}
}
-/// Specifies a run of strips inside a draw that can be drawn with the same external texture
-/// binding.
+/// Specifies a run of strips that can be drawn with the same external texture binding.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ExternalTextureRun {
/// External texture bound for the run.
pub(crate) texture_id: TextureId,
/// Start index of the strip range for this run. The end is implicitly the start of the next
- /// run, or, for the last run, the total number of strips.
+ /// run, or, for the last run, the total number of strips in the pass.
pub(crate) strips_start: usize,
}
+fn push_external_texture_run(
+ runs: &mut Vec<ExternalTextureRun>,
+ strips_len: usize,
+ external_texture_id: Option<TextureId>,
+) {
+ let Some(texture_id) = external_texture_id else {
+ return;
+ };
+ if runs.last().is_some_and(|run| run.texture_id == texture_id) {
+ return;
+ }
+
+ runs.push(ExternalTextureRun {
+ texture_id,
+ strips_start: if runs.is_empty() { 0 } else { strips_len },
+ });
+}
+
/// Assigns monotonically increasing depth values to opaque strips.
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct DepthCounter {
@@ -466,7 +524,8 @@
#[cfg(test)]
mod tests {
- use super::{Draw, DrawBuffers, DrawBuilder, DrawState, ExternalTextureRun};
+ use super::{Draw, DrawBuffers, DrawBuilder, DrawState, ExternalTextureRun, OpaqueDraw};
+ use crate::GpuStrip;
use crate::paint::PaintResolver;
use crate::scene::{RecordedDraw, RecordedRect};
use crate::target::{
@@ -540,6 +599,29 @@
PaintResolver::new(&[], &[])
}
+ fn gpu_strip(x: u16) -> GpuStrip {
+ GpuStrip {
+ x,
+ y: 0,
+ width: 1,
+ dense_width_or_rect_height: 0,
+ col_idx_or_rect_frac: 0,
+ payload: 0,
+ paint_and_rect_flag: 0,
+ depth_index: 0,
+ }
+ }
+
+ fn strip_xs(draw: &OpaqueDraw) -> Vec<u16> {
+ draw.strips().iter().map(|strip| strip.x).collect()
+ }
+
+ fn run_starts(runs: &[ExternalTextureRun]) -> Vec<(TextureId, usize)> {
+ runs.iter()
+ .map(|run| (run.texture_id, run.strips_start))
+ .collect()
+ }
+
fn external(texture_id: TextureId) -> EncodedPaint {
EncodedPaint::ExternalTexture(EncodedExternalTexture {
texture_id,
@@ -603,21 +685,8 @@
}
assert_eq!(
- draw.external_texture_runs,
- [
- ExternalTextureRun {
- texture_id: texture_a,
- strips_start: 0,
- },
- ExternalTextureRun {
- texture_id: texture_b,
- strips_start: 2,
- },
- ExternalTextureRun {
- texture_id: texture_a,
- strips_start: 4,
- },
- ]
+ run_starts(&draw.external_texture_runs),
+ [(texture_a, 0), (texture_b, 2), (texture_a, 4)]
);
}
@@ -636,13 +705,76 @@
assert_eq!(draw.strip_ranges.len(), 3);
// Images in the atlas are handled separately from external textures, so
// it's fine to collapse them.
+ assert_eq!(run_starts(&draw.external_texture_runs), [(texture, 0)]);
+ }
+
+ #[test]
+ fn opaque_reverse_rebases_texture_runs() {
+ let texture_a = TextureId(10);
+ let texture_b = TextureId(20);
+ let texture_c = TextureId(30);
+ let mut draw = OpaqueDraw::default();
+
+ for (x, texture_id) in [
+ (0, None),
+ (1, Some(texture_a)),
+ (2, Some(texture_a)),
+ (3, None),
+ (4, None),
+ (5, None),
+ (6, Some(texture_b)),
+ (7, Some(texture_b)),
+ (8, Some(texture_c)),
+ (9, None),
+ (10, Some(texture_c)),
+ (11, None),
+ (12, None),
+ (13, Some(texture_a)),
+ (14, None),
+ (15, Some(texture_b)),
+ ] {
+ draw.push(gpu_strip(x), texture_id);
+ }
assert_eq!(
- draw.external_texture_runs,
- [ExternalTextureRun {
- texture_id: texture,
- strips_start: 0,
- }]
+ run_starts(draw.external_texture_runs()),
+ [
+ (texture_a, 0),
+ (texture_b, 6),
+ (texture_c, 8),
+ (texture_a, 13),
+ (texture_b, 15),
+ ]
);
+
+ draw.reverse();
+
+ assert_eq!(
+ strip_xs(&draw),
+ [15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
+ );
+ assert_eq!(
+ run_starts(draw.external_texture_runs()),
+ [
+ (texture_b, 0),
+ (texture_a, 1),
+ (texture_c, 3),
+ (texture_b, 8),
+ (texture_a, 10),
+ ]
+ );
+ }
+
+ #[test]
+ fn opaque_reverse_without_texture_runs() {
+ let mut draw = OpaqueDraw::default();
+ for x in 0..3 {
+ draw.push(gpu_strip(x), None);
+ }
+
+ draw.reverse();
+
+ assert_eq!(strip_xs(&draw), [2, 1, 0]);
+ assert_eq!(run_starts(draw.external_texture_runs()), []);
}
#[test]
@@ -683,14 +815,14 @@
no_paints(),
);
- assert_eq!(user_case.buffers.opaque_strips.len(), 1);
+ assert_eq!(user_case.buffers.opaque.strips().len(), 1);
assert_eq!(user_draw.strip_ranges.len(), 1);
let mut atlas_case = DrawCase::new(RootTarget::AtlasLayer, RectU16::new(0, 0, 8, 8));
let mut atlas_draw = Draw::default();
atlas_case.rect(&mut atlas_draw, rect(0.0), solid(1.0), no_paints());
- assert!(atlas_case.buffers.opaque_strips.is_empty());
+ assert!(atlas_case.buffers.opaque.is_empty());
assert_eq!(atlas_draw.strip_ranges.len(), 1);
}
@@ -726,7 +858,8 @@
);
assert_eq!(
case.buffers
- .opaque_strips
+ .opaque
+ .strips()
.iter()
.map(|strip| strip.depth_index)
.collect::<Vec<_>>(),
diff --git a/sparse_strips/vello_hybrid/src/render/webgl/mod.rs b/sparse_strips/vello_hybrid/src/render/webgl/mod.rs
index d66c3e6..faae943 100644
--- a/sparse_strips/vello_hybrid/src/render/webgl/mod.rs
+++ b/sparse_strips/vello_hybrid/src/render/webgl/mod.rs
@@ -2841,12 +2841,7 @@
if opaque_count > 0 {
self.gl.depth_mask(true);
self.gl.disable(WebGl2RenderingContext::BLEND);
- self.gl.draw_arrays_instanced(
- WebGl2RenderingContext::TRIANGLE_STRIP,
- 0,
- 4,
- opaque_count,
- );
+ self.draw_strips(external_texture_runs, 0, opaque_count);
}
// Alpha pass: back-to-front, depth test ON, depth write OFF, blend ON.
@@ -3135,11 +3130,15 @@
}
impl Backend for WebGlRendererContext<'_> {
- fn opaque_draw_pass(&mut self, strips: &[GpuStrip]) {
+ fn opaque_draw_pass(
+ &mut self,
+ strips: &[GpuStrip],
+ external_texture_runs: &[ExternalTextureRun],
+ ) {
self.strip_pass_inner(
strips,
RangedSlice::empty(),
- &[],
+ external_texture_runs,
DrawPassTarget::Root(RootTarget::UserSurface),
None,
);
diff --git a/sparse_strips/vello_hybrid/src/render/wgpu/mod.rs b/sparse_strips/vello_hybrid/src/render/wgpu/mod.rs
index d3eb00d..b15b60b 100644
--- a/sparse_strips/vello_hybrid/src/render/wgpu/mod.rs
+++ b/sparse_strips/vello_hybrid/src/render/wgpu/mod.rs
@@ -2866,6 +2866,31 @@
render_pass.set_bind_group(3, &self.programs.resources.gradient_bind_group, &[]);
render_pass.set_vertex_buffer(0, self.programs.resources.strips_buffer.slice(..));
+ let draw_strip_runs = |render_pass: &mut wgpu::RenderPass<'_>, first_instance, count| {
+ if external_texture_runs.is_empty() {
+ render_pass.set_bind_group(1, &self.programs.resources.atlas_bind_group, &[]);
+ render_pass.draw(0..4, first_instance..first_instance + count);
+
+ return;
+ }
+
+ // Each run is drawn with a different external texture binding. Runs go from
+ // `run.strips_start` to the next run's `strips_start`; the last run goes to the end of
+ // the strips buffer.
+ for (i, run) in external_texture_runs.iter().enumerate() {
+ let paint_source_bind_group = self
+ .external_paint_source_bind_groups
+ .get(&run.texture_id)
+ .unwrap();
+ render_pass.set_bind_group(1, paint_source_bind_group, &[]);
+ let start = u32::try_from(run.strips_start).unwrap();
+ let end = external_texture_runs
+ .get(i + 1)
+ .map_or(count, |next| u32::try_from(next.strips_start).unwrap());
+ render_pass.draw(0..4, first_instance + start..first_instance + end);
+ }
+ };
+
if opaque_count > 0 {
// Opaque pass
debug_assert!(
@@ -2873,8 +2898,7 @@
"opaque strips require the final view depth attachment"
);
render_pass.set_pipeline(&self.programs.opaque_strip_pipeline);
- render_pass.set_bind_group(1, &self.programs.resources.atlas_bind_group, &[]);
- render_pass.draw(0..4, 0..opaque_count);
+ draw_strip_runs(&mut render_pass, 0, opaque_count);
}
if alpha_count > 0 {
@@ -2890,29 +2914,7 @@
render_pass.set_pipeline(&self.programs.intermediate_strip_pipeline);
}
- let alpha_start = opaque_count;
- if external_texture_runs.is_empty() {
- render_pass.set_bind_group(1, &self.programs.resources.atlas_bind_group, &[]);
- render_pass.draw(0..4, alpha_start..alpha_start + alpha_count);
- } else {
- // Each run is drawn with a different external texture binding. Runs go from
- // `run.strips_start` to the next run's `strips_start`; the last run goes to the end of
- // the strips buffer.
- for (i, run) in external_texture_runs.iter().enumerate() {
- let paint_source_bind_group = self
- .external_paint_source_bind_groups
- .get(&run.texture_id)
- .unwrap();
- render_pass.set_bind_group(1, paint_source_bind_group, &[]);
- let start = u32::try_from(run.strips_start).unwrap();
- let end = external_texture_runs
- .get(i + 1)
- .map_or(alpha_count, |next| {
- u32::try_from(next.strips_start).unwrap()
- });
- render_pass.draw(0..4, alpha_start + start..alpha_start + end);
- }
- }
+ draw_strip_runs(&mut render_pass, opaque_count, alpha_count);
}
}
@@ -3129,11 +3131,15 @@
}
impl Backend for RendererContext<'_> {
- fn opaque_draw_pass(&mut self, strips: &[GpuStrip]) {
+ fn opaque_draw_pass(
+ &mut self,
+ strips: &[GpuStrip],
+ external_texture_runs: &[ExternalTextureRun],
+ ) {
self.strip_pass_inner(
strips,
RangedSlice::empty(),
- &[],
+ external_texture_runs,
DrawPassTarget::Root(RootTarget::UserSurface),
None,
);
diff --git a/sparse_strips/vello_hybrid/src/sampling.rs b/sparse_strips/vello_hybrid/src/sampling.rs
index 6e01452..812eb84 100644
--- a/sparse_strips/vello_hybrid/src/sampling.rs
+++ b/sparse_strips/vello_hybrid/src/sampling.rs
@@ -13,9 +13,36 @@
/// Source region in texel coordinates.
pub source_region: RectU16,
+ /// Whether the sampled source region may contain non-opaque pixels.
+ ///
+ /// Only set this to `false` if every pixel is guaranteed to be opaque (e.g. in a texture
+ /// generated from a JPEG image). If you set this to `false` even though there are non-opaque
+ /// pixels, you will get wrong rendering.
+ ///
+ /// If unsure, always set this to `true`.
+ pub may_have_transparency: bool,
+
/// Transform mapping the local source region to the destination.
///
/// This maps from the *local* rectangle into the destination, ignoring the origin of
/// [`Self::source_region`].
pub transform: Affine,
}
+
+impl SampleRect {
+ /// Create a new [`SampleRect`].
+ pub fn new(source_region: RectU16, transform: Affine) -> Self {
+ Self {
+ source_region,
+ may_have_transparency: true,
+ transform,
+ }
+ }
+
+ /// Indicate that the sample rect only contains opaque pixels.
+ #[must_use]
+ pub fn with_opaque_hint(mut self) -> Self {
+ self.may_have_transparency = false;
+ self
+ }
+}
diff --git a/sparse_strips/vello_hybrid/src/scene.rs b/sparse_strips/vello_hybrid/src/scene.rs
index a637d88..d6a8504 100644
--- a/sparse_strips/vello_hybrid/src/scene.rs
+++ b/sparse_strips/vello_hybrid/src/scene.rs
@@ -311,22 +311,27 @@
x_extend: Extend,
y_extend: Extend,
transform: Affine,
+ may_have_transparency: bool,
) -> Paint {
let idx = self.encoded_paints.len();
+ let sampler = ImageSampler {
+ x_extend,
+ y_extend,
+ quality,
+ alpha: 1.0,
+ };
+ let has_opacity = self
+ .render_state
+ .tint
+ .is_some_and(|tint| tint.color.components[3] < 1.0)
+ // Not supported yet, but just to future-proof.
+ || sampler.alpha != 1.0;
+
let encoded = EncodedExternalTexture {
texture_id,
source_region,
- sampler: ImageSampler {
- x_extend,
- y_extend,
- quality,
- alpha: 1.0,
- },
- // TODO: Make this configurable by the user, and also take `ImageSampler` alpha into
- // account.
- // **IMPORTANT**: If this ever can become false, we need to make sure to update
- // Vello Hybrid so the opaque pass supports external textures as well!
- may_have_transparency: true,
+ sampler,
+ may_have_transparency: may_have_transparency || has_opacity,
transform: transform.inverse(),
tint: self.render_state.tint,
};
@@ -549,6 +554,7 @@
x_extend,
y_extend,
transform,
+ rect.may_have_transparency,
);
ctx.recorder
@@ -561,6 +567,7 @@
x_extend,
y_extend,
transform,
+ rect.may_have_transparency,
);
let dst_rect = Rect::new(0., 0., w, h);
ctx.fill_path_with(
diff --git a/sparse_strips/vello_hybrid/src/schedule/execute.rs b/sparse_strips/vello_hybrid/src/schedule/execute.rs
index 969431a..54e6386 100644
--- a/sparse_strips/vello_hybrid/src/schedule/execute.rs
+++ b/sparse_strips/vello_hybrid/src/schedule/execute.rs
@@ -21,7 +21,11 @@
/// ever called, it's called before any of the other ones and only once.
///
/// The strips are guaranteed to be non-empty.
- fn opaque_draw_pass(&mut self, strips: &[GpuStrip]);
+ fn opaque_draw_pass(
+ &mut self,
+ strips: &[GpuStrip],
+ external_texture_runs: &[ExternalTextureRun],
+ );
/// Execute a draw pass against the given target.
///
@@ -78,9 +82,12 @@
filter_plan: &mut FilterPassPlan,
) {
if DrawPassTarget::Root(root_output_target).enable_opaque()
- && !buffers.draw_buffers.opaque_strips.is_empty()
+ && !buffers.draw_buffers.opaque.is_empty()
{
- renderer.opaque_draw_pass(&buffers.draw_buffers.opaque_strips);
+ renderer.opaque_draw_pass(
+ buffers.draw_buffers.opaque.strips(),
+ buffers.draw_buffers.opaque.external_texture_runs(),
+ );
}
self.rounds.execute(
@@ -209,7 +216,11 @@
}
impl Backend for Recorder {
- fn opaque_draw_pass(&mut self, _strips: &[GpuStrip]) {
+ fn opaque_draw_pass(
+ &mut self,
+ _strips: &[GpuStrip],
+ _external_texture_runs: &[ExternalTextureRun],
+ ) {
self.calls.push(Call::Opaque);
}
diff --git a/sparse_strips/vello_hybrid/src/schedule/mod.rs b/sparse_strips/vello_hybrid/src/schedule/mod.rs
index 2264422..a5585b4 100644
--- a/sparse_strips/vello_hybrid/src/schedule/mod.rs
+++ b/sparse_strips/vello_hybrid/src/schedule/mod.rs
@@ -322,7 +322,7 @@
self.schedule_root(&mut rounds)?;
// Since the strips should be rendered front-to-back.
- self.storage.buffers.draw_buffers.opaque_strips.reverse();
+ self.storage.buffers.draw_buffers.opaque.reverse();
#[cfg(any(test, debug_assertions))]
rounds.validate(&self.storage.buffers);
diff --git a/sparse_strips/vello_hybrid/src/schedule/schedule_tests.rs b/sparse_strips/vello_hybrid/src/schedule/schedule_tests.rs
index 824334e..bbae1a2 100644
--- a/sparse_strips/vello_hybrid/src/schedule/schedule_tests.rs
+++ b/sparse_strips/vello_hybrid/src/schedule/schedule_tests.rs
@@ -884,7 +884,7 @@
.unwrap();
assert_eq!(storage.buffers.draw_buffers.strips.len(), 1);
- assert!(storage.buffers.draw_buffers.opaque_strips.is_empty());
+ assert!(storage.buffers.draw_buffers.opaque.is_empty());
assert!(storage.buffers.blend_ops.is_empty());
assert!(storage.buffers.blend_strips.is_empty());
assert!(storage.buffers.filter_ops.is_empty());
diff --git a/sparse_strips/vello_hybrid/src/schedule/test_support.rs b/sparse_strips/vello_hybrid/src/schedule/test_support.rs
index 80cc180..5fcfabd 100644
--- a/sparse_strips/vello_hybrid/src/schedule/test_support.rs
+++ b/sparse_strips/vello_hybrid/src/schedule/test_support.rs
@@ -204,7 +204,8 @@
self.storage
.buffers
.draw_buffers
- .opaque_strips
+ .opaque
+ .strips()
.iter()
.map(|strip| strip.x)
.collect()
diff --git a/sparse_strips/vello_sparse_tests/snapshots/external_texture_root_painter_order.png b/sparse_strips/vello_sparse_tests/snapshots/external_texture_root_painter_order.png
new file mode 100644
index 0000000..d2bf0db
--- /dev/null
+++ b/sparse_strips/vello_sparse_tests/snapshots/external_texture_root_painter_order.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:15b84b78e387a1a0b49cd9d9af887eb62fe5f4725baada75926ec46d34883b6c
+size 334
diff --git a/sparse_strips/vello_sparse_tests/tests/external_texture.rs b/sparse_strips/vello_sparse_tests/tests/external_texture.rs
index 006adc9..8a1bd2f 100644
--- a/sparse_strips/vello_sparse_tests/tests/external_texture.rs
+++ b/sparse_strips/vello_sparse_tests/tests/external_texture.rs
@@ -1,8 +1,6 @@
// Copyright 2026 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
-// TODO: Increase test coverage to cover things like tinted external textures, etc.
-
mod tests {
use std::sync::Arc;
@@ -11,8 +9,8 @@
use vello_common::filter_effects::{EdgeMode, Filter, FilterPrimitive};
use vello_common::geometry::RectU16;
use vello_common::kurbo::{Affine, Circle, Rect, Shape};
- use vello_common::paint::{Image, ImageSource};
- use vello_common::peniko::{Extend, ImageQuality, ImageSampler};
+ use vello_common::paint::{Image, ImageSource, Tint, TintMode};
+ use vello_common::peniko::{Color, Extend, ImageQuality, ImageSampler};
use vello_common::pixmap::Pixmap;
use vello_dev_macros::vello_test;
use vello_hybrid::SampleRect;
@@ -39,11 +37,26 @@
))
}
+ fn texture_rect(
+ x: f64,
+ y: f64,
+ width: f64,
+ height: f64,
+ may_have_transparency: bool,
+ ) -> SampleRect {
+ let rect = SampleRect::new(
+ RectU16::new(0, 0, 1, 1),
+ Affine::translate((x, y)) * Affine::scale_non_uniform(width, height),
+ );
+ if may_have_transparency {
+ rect
+ } else {
+ rect.with_opaque_hint()
+ }
+ }
+
fn texture_rect_at(x: f64, y: f64) -> [SampleRect; 1] {
- [SampleRect {
- source_region: RectU16::new(0, 0, 1, 1),
- transform: Affine::translate((x, y)) * Affine::scale(40.),
- }]
+ [texture_rect(x, y, 40., 40., true)]
}
fn draw_atlas_rect(ctx: &mut impl Renderer, image: ImageSource, rect: Rect) {
@@ -72,11 +85,11 @@
ctx.draw_texture_rects(
texture_id,
ImageQuality::Medium,
- [SampleRect {
- source_region: RectU16::new(0, 0, 1, 1),
- transform: Affine::translate((rect.x0, rect.y0))
+ [SampleRect::new(
+ RectU16::new(0, 0, 1, 1),
+ Affine::translate((rect.x0, rect.y0))
* Affine::scale_non_uniform(rect.width(), rect.height()),
- }],
+ )],
);
ctx.pop_clip_path();
}
@@ -92,20 +105,14 @@
ctx.draw_texture_rects(
texture_id,
ImageQuality::Low,
- [SampleRect {
- source_region: SPRITES[0],
- transform: Affine::translate((12., 15.)),
- }],
+ [SampleRect::new(SPRITES[0], Affine::translate((12., 15.)))],
);
ctx.set_paint(color::palette::css::PALE_GOLDENROD.with_alpha(0.7));
ctx.fill_rect(&Rect::new(10., 7., 65., 55.));
ctx.draw_texture_rects(
texture_id,
ImageQuality::Low,
- [SampleRect {
- source_region: SPRITES[3],
- transform: Affine::translate((25., 25.)),
- }],
+ [SampleRect::new(SPRITES[3], Affine::translate((25., 25.)))],
);
}
@@ -139,6 +146,65 @@
ctx.draw_texture_rects(red_texture, ImageQuality::Low, texture_rect_at(48., 48.));
}
+ #[vello_test(hybrid_only, hybrid_no_depth)]
+ fn external_texture_root_painter_order(ctx: &mut impl Renderer) {
+ let coral = ctx.register_external_texture(solid_pixmap(225, 87, 89, 255));
+ let teal = ctx.register_external_texture(solid_pixmap(42, 157, 143, 255));
+ let navy = ctx.register_external_texture(solid_pixmap(38, 70, 83, 255));
+ let gold = ctx.register_external_texture(solid_pixmap(153, 102, 61, 160));
+ let tint_source = ctx.register_external_texture(solid_pixmap(255, 255, 255, 255));
+
+ ctx.draw_texture_rects(
+ coral,
+ ImageQuality::Low,
+ [texture_rect(6., 6., 88., 88., false)],
+ );
+ ctx.draw_texture_rects(
+ gold,
+ ImageQuality::Low,
+ [
+ texture_rect(11.5, 33.5, 77., 33., true),
+ texture_rect(33.5, 11.5, 33., 77., true),
+ ],
+ );
+ ctx.draw_texture_rects(
+ teal,
+ ImageQuality::Low,
+ [texture_rect(21.5, 21.5, 57., 57., false)],
+ );
+ ctx.set_tint(Some(Tint {
+ color: Color::from_rgba8(128, 102, 204, 160),
+ mode: TintMode::Multiply,
+ }));
+ ctx.draw_texture_rects(
+ tint_source,
+ ImageQuality::Low,
+ [
+ texture_rect(16., 38., 68., 24., false),
+ texture_rect(38., 16., 24., 68., false),
+ ],
+ );
+ ctx.set_tint(None);
+ ctx.draw_texture_rects(
+ navy,
+ ImageQuality::Low,
+ [texture_rect(33.5, 33.5, 33., 33., false)],
+ );
+ ctx.draw_texture_rects(
+ gold,
+ ImageQuality::Low,
+ [
+ texture_rect(28., 44., 44., 12., true),
+ texture_rect(44., 28., 12., 44., true),
+ ],
+ );
+ ctx.draw_texture_rects(
+ coral,
+ ImageQuality::Low,
+ [texture_rect(44., 44., 12., 12., false)],
+ );
+ }
+
#[vello_test(hybrid_only)]
fn external_texture_atlas_interleaving(ctx: &mut impl Renderer) {
let atlas_red = ctx.get_image_source(solid_pixmap(254, 0, 0, 254));
@@ -151,19 +217,19 @@
ctx.draw_texture_rects(
external_green,
ImageQuality::Low,
- [SampleRect {
- source_region: RectU16::new(0, 0, 1, 1),
- transform: Affine::translate((66., 10.)) * Affine::scale(24.),
- }],
+ [SampleRect::new(
+ RectU16::new(0, 0, 1, 1),
+ Affine::translate((66., 10.)) * Affine::scale(24.),
+ )],
);
draw_atlas_rect(ctx, atlas_blue, Rect::new(38., 38., 62., 62.));
ctx.draw_texture_rects(
external_yellow,
ImageQuality::Low,
- [SampleRect {
- source_region: RectU16::new(0, 0, 1, 1),
- transform: Affine::translate((10., 66.)) * Affine::scale(24.),
- }],
+ [SampleRect::new(
+ RectU16::new(0, 0, 1, 1),
+ Affine::translate((10., 66.)) * Affine::scale(24.),
+ )],
);
draw_atlas_rect(ctx, atlas_magenta, Rect::new(66., 66., 90., 90.));
}
@@ -279,10 +345,10 @@
ctx.draw_texture_rects(
texture_id,
ImageQuality::High,
- [SampleRect {
- source_region: SPRITES[0],
- transform: Affine::translate((15., 15.)) * Affine::skew(0.2, 0.1),
- }],
+ [SampleRect::new(
+ SPRITES[0],
+ Affine::translate((15., 15.)) * Affine::skew(0.2, 0.1),
+ )],
);
}
@@ -296,14 +362,8 @@
texture_id,
ImageQuality::Medium,
[
- SampleRect {
- source_region: SPRITES[1],
- transform: Affine::translate((18., 18.)),
- },
- SampleRect {
- source_region: SPRITES[3],
- transform: Affine::translate((34., 34.)),
- },
+ SampleRect::new(SPRITES[1], Affine::translate((18., 18.))),
+ SampleRect::new(SPRITES[3], Affine::translate((34., 34.))),
],
);
ctx.pop_layer();
@@ -321,10 +381,7 @@
ctx.draw_texture_rects(
texture_id,
ImageQuality::Low,
- [SampleRect {
- source_region: SPRITES[2],
- transform: Affine::translate((20., 20.)),
- }],
+ [SampleRect::new(SPRITES[2], Affine::translate((20., 20.)))],
);
ctx.pop_layer();
}
@@ -350,9 +407,8 @@
ctx.draw_texture_rects(
texture_id,
ImageQuality::Low,
- placements.map(|(source_region, x, y)| SampleRect {
- source_region,
- transform: Affine::translate((x, y)),
+ placements.map(|(source_region, x, y)| {
+ SampleRect::new(source_region, Affine::translate((x, y)))
}),
);
}
@@ -370,18 +426,12 @@
texture_id,
ImageQuality::Medium,
[
- SampleRect {
- source_region: SPRITES[0],
- transform: Affine::translate((6., 8.)),
- },
- SampleRect {
- source_region: SPRITES[3],
- transform: Affine::translate((28., 5.)) * Affine::skew(0.18, -0.08),
- },
- SampleRect {
- source_region: SPRITES[2],
- transform: Affine::translate((48., 6.)),
- },
+ SampleRect::new(SPRITES[0], Affine::translate((6., 8.))),
+ SampleRect::new(
+ SPRITES[3],
+ Affine::translate((28., 5.)) * Affine::skew(0.18, -0.08),
+ ),
+ SampleRect::new(SPRITES[2], Affine::translate((48., 6.))),
],
);
}