diff --git a/Cargo.lock b/Cargo.lock index 5e63d1ce8..710677c9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -330,9 +330,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arbitrary" diff --git a/bin/gosub-screenshot/main.rs b/bin/gosub-screenshot/main.rs index 3c8e10c4c..067c545bf 100644 --- a/bin/gosub-screenshot/main.rs +++ b/bin/gosub-screenshot/main.rs @@ -340,7 +340,7 @@ fn main() { tiles.len() ); - // Fill with opaque white, then alpha-blend each tile (premultiplied RGBA8 from Vello). + // Fill with opaque white, then alpha-blend each tile (premultiplied). let mut pixels = vec![255u8; (page_w * page_h * 4) as usize]; for tile in tiles.iter() { @@ -351,7 +351,11 @@ fn main() { } let tw = tile.width.min(page_w - tx) as usize; let th = tile.height.min(page_h - ty) as usize; - let data = &tile.data; + // Normalize to [R, G, B, A] regardless of which rasterizer produced the tile. + // Under `cargo build --all`, Cargo feature unification can select the Cairo + // rasterizer (ARGB32 / [B, G, R, A]) even though this binary asks for Vello; + // honoring the tagged format keeps colors correct either way. + let data = tile.format.to_rgba(&tile.data); for row in 0..th { for col in 0..tw { diff --git a/crates/gosub_engine/src/engine/context.rs b/crates/gosub_engine/src/engine/context.rs index acb0392cf..d0fb3b28a 100644 --- a/crates/gosub_engine/src/engine/context.rs +++ b/crates/gosub_engine/src/engine/context.rs @@ -180,7 +180,9 @@ struct BakedTile { page_y: f64, width: u32, height: u32, - data: Arc>, + data: bytes::Bytes, + /// In-memory byte order of `data`, set by the rasterizer that produced it. + format: gosub_render_pipeline::render::backend::PixelFormat, } /// Key that uniquely identifies a tile's content for cache lookup. @@ -200,7 +202,7 @@ struct PipelineCache { /// Rasterized tile data keyed by (page_x, page_y, layer_id, content_hash). /// Passed to the next render so unchanged tiles skip rasterization. /// Value is (physical_width, physical_height, pixel_data). - tile_pixel_cache: std::collections::HashMap>)>, + tile_pixel_cache: std::collections::HashMap, } /// BrowsingContext dedicated to a specific tab @@ -932,7 +934,7 @@ fn pipeline_build_cache( #[cfg(feature = "backend_vello")] _vello_resources: Option< std::sync::Arc, >, - prev_tile_cache: std::collections::HashMap>)>, + prev_tile_cache: std::collections::HashMap, ) -> PipelineCache { use gosub_render_pipeline::common::browser_state::{BrowserState, WireframeState}; use gosub_render_pipeline::common::document::pipeline_doc::GosubDocumentAdapter; @@ -1084,9 +1086,13 @@ fn pipeline_build_cache( use gosub_render_pipeline::common::media::MediaStore; use gosub_render_pipeline::common::texture_store::TextureStore; use gosub_render_pipeline::rasterizer::Rasterable; + use gosub_render_pipeline::render::backend::PixelFormat; use gosub_render_pipeline::tiler::TileId; use rayon::prelude::*; + // Cairo and Skia both emit premultiplied ARGB32 (BGRA byte order). + let tile_format = PixelFormat::PreMulArgb32; + let t = Instant::now(); let ts6 = timing_start!("pipeline.rasterize"); let media_store = MediaStore::new(); @@ -1103,7 +1109,7 @@ fn pipeline_build_cache( // For each tile: compute a content hash; if it matches the previous render's cached // pixels, reuse them (cache hit). Otherwise rasterize on this thread. // Result: (tile_id, Option, Option) - type CacheEntry = (TileCacheKey, (u32, u32, Arc>)); + type CacheEntry = (TileCacheKey, (u32, u32, bytes::Bytes)); let results: Vec<(TileId, Option, Option)> = dirty_ids .par_iter() .map(|&tile_id| { @@ -1120,7 +1126,8 @@ fn pipeline_build_cache( page_y: tile.rect.y, width: w, height: h, - data: Arc::clone(data), + data: data.clone(), + format: tile_format, }; return (tile_id, Some(baked), None); } @@ -1135,10 +1142,11 @@ fn pipeline_build_cache( page_y: tile.rect.y, width: tex.width as u32, height: tex.height as u32, - data: Arc::clone(&tex.data), + data: tex.data.clone(), + format: tex.format, }); - let cache_entry = baked.as_ref().map(|b| (key, (b.width, b.height, Arc::clone(&b.data)))); + let cache_entry = baked.as_ref().map(|b| (key, (b.width, b.height, b.data.clone()))); (tile_id, baked, cache_entry) }) .collect(); @@ -1148,7 +1156,7 @@ fn pipeline_build_cache( let mut cache_hits = 0usize; let mut empty = 0usize; let mut tiles: Vec = Vec::with_capacity(results.len()); - let mut new_tile_cache: std::collections::HashMap>)> = + let mut new_tile_cache: std::collections::HashMap = std::collections::HashMap::with_capacity(results.len()); for (tile_id, baked, cache_entry) in results { @@ -1252,7 +1260,8 @@ fn pipeline_build_cache( page_y: tile.rect.y, width: tex.width as u32, height: tex.height as u32, - data: Arc::clone(&tex.data), + data: tex.data.clone(), + format: tex.format, }); } } @@ -1278,13 +1287,13 @@ fn pipeline_build_cache( not(feature = "backend_cairo"), not(feature = "backend_skia") ))] - let new_tile_cache: std::collections::HashMap>)> = + let new_tile_cache: std::collections::HashMap = std::collections::HashMap::new(); #[cfg(not(any(feature = "backend_cairo", feature = "backend_skia", feature = "backend_vello")))] let baked_tiles: Vec = Vec::new(); #[cfg(not(any(feature = "backend_cairo", feature = "backend_skia", feature = "backend_vello")))] - let new_tile_cache: std::collections::HashMap>)> = + let new_tile_cache: std::collections::HashMap = std::collections::HashMap::new(); timing_stop!(ts_total); @@ -1303,7 +1312,8 @@ fn pipeline_build_cache( page_y: t.page_y as f32, width: t.width, height: t.height, - data: Arc::clone(&t.data), + data: t.data.clone(), + format: t.format, }) .collect::>(), ); @@ -1334,7 +1344,7 @@ fn pipeline_hover_repaint( #[cfg(feature = "backend_vello")] _vello_resources: Option< std::sync::Arc, >, - prev_tile_cache: std::collections::HashMap>)>, + prev_tile_cache: std::collections::HashMap, ) -> PipelineCache { use gosub_render_pipeline::common::browser_state::{BrowserState, WireframeState}; use gosub_render_pipeline::common::geo::{Dimension as PipelineDimension, Rect as PipelineRect}; @@ -1426,7 +1436,8 @@ fn pipeline_hover_repaint( page_y: t.page_y as f32, width: t.width, height: t.height, - data: Arc::clone(&t.data), + data: t.data.clone(), + format: t.format, }) .collect::>(), ); @@ -1503,9 +1514,13 @@ fn pipeline_hover_repaint( use gosub_render_pipeline::common::media::MediaStore; use gosub_render_pipeline::common::texture_store::TextureStore; use gosub_render_pipeline::rasterizer::Rasterable; + use gosub_render_pipeline::render::backend::PixelFormat; use gosub_render_pipeline::tiler::TileId; use rayon::prelude::*; + // Cairo and Skia both emit premultiplied ARGB32 (BGRA byte order). + let tile_format = PixelFormat::PreMulArgb32; + let t = Instant::now(); let ts6 = timing_start!("pipeline.hover.rasterize"); let media_store = MediaStore::new(); @@ -1524,7 +1539,7 @@ fn pipeline_hover_repaint( layer_ids.len() ); - type CacheEntry = (TileCacheKey, (u32, u32, Arc>)); + type CacheEntry = (TileCacheKey, (u32, u32, bytes::Bytes)); let results: Vec<(TileId, Option, Option)> = dirty_ids .par_iter() .map(|&tile_id| { @@ -1540,7 +1555,8 @@ fn pipeline_hover_repaint( page_y: tile.rect.y, width: w, height: h, - data: Arc::clone(data), + data: data.clone(), + format: tile_format, }), None, ); @@ -1554,9 +1570,10 @@ fn pipeline_hover_repaint( page_y: tile.rect.y, width: tex.width as u32, height: tex.height as u32, - data: Arc::clone(&tex.data), + data: tex.data.clone(), + format: tex.format, }); - let cache_entry = baked.as_ref().map(|b| (key, (b.width, b.height, Arc::clone(&b.data)))); + let cache_entry = baked.as_ref().map(|b| (key, (b.width, b.height, b.data.clone()))); (tile_id, baked, cache_entry) }) .collect(); @@ -1564,7 +1581,7 @@ fn pipeline_hover_repaint( let mut rasterized = 0usize; let mut cache_hits = 0usize; let mut tiles: Vec = Vec::with_capacity(results.len()); - let mut new_tile_cache: std::collections::HashMap>)> = + let mut new_tile_cache: std::collections::HashMap = std::collections::HashMap::with_capacity(results.len()); for (tile_id, baked, cache_entry) in results { if let Some(tile) = tile_list.arena.get_mut(&tile_id) { @@ -1652,7 +1669,8 @@ fn pipeline_hover_repaint( page_y: tile.rect.y, width: tex.width as u32, height: tex.height as u32, - data: Arc::clone(&tex.data), + data: tex.data.clone(), + format: tex.format, }); } } @@ -1669,7 +1687,7 @@ fn pipeline_hover_repaint( #[cfg(not(any(feature = "backend_cairo", feature = "backend_skia", feature = "backend_vello")))] let (baked_tiles, new_tile_cache): ( Vec, - std::collections::HashMap>)>, + std::collections::HashMap, ) = (Vec::new(), std::collections::HashMap::new()); // Merge: newly rasterized hover tiles + clean tiles carried from previous render. @@ -1690,7 +1708,8 @@ fn pipeline_hover_repaint( page_y: t.page_y as f32, width: t.width, height: t.height, - data: Arc::clone(&t.data), + data: t.data.clone(), + format: t.format, }) .collect::>(), ); @@ -1734,7 +1753,8 @@ fn pipeline_composite(cache: &PipelineCache, scroll_x: f64, scroll_y: f64, vp_w: y: (tile.page_y - scroll_y) as f32, w: tile.width, h: tile.height, - data: Arc::clone(&tile.data), + data: tile.data.clone(), + format: tile.format, }); blits += 1; } diff --git a/crates/gosub_render_pipeline/src/common/texture.rs b/crates/gosub_render_pipeline/src/common/texture.rs index d742b2d71..30301e700 100644 --- a/crates/gosub_render_pipeline/src/common/texture.rs +++ b/crates/gosub_render_pipeline/src/common/texture.rs @@ -25,12 +25,14 @@ impl std::fmt::Display for TextureId { } } -/// Raw pixel buffer produced by a rasterizer. Data is Arc-wrapped so it can be shared -/// zero-copy into BakedTile / CachedTile without any pixel buffer copies. +/// Raw pixel buffer produced by a rasterizer. Stored as `Bytes` so it can be cloned and +/// shared (cheap refcount bump) and sliced zero-copy without any pixel buffer copies. #[derive(Debug)] pub struct Texture { pub id: TextureId, pub width: usize, pub height: usize, - pub data: std::sync::Arc>, + pub data: bytes::Bytes, + /// In-memory byte order of `data`, set by the rasterizer that produced it. + pub format: crate::render::backend::PixelFormat, } diff --git a/crates/gosub_render_pipeline/src/common/texture_store.rs b/crates/gosub_render_pipeline/src/common/texture_store.rs index 6d8365754..c8f7729c9 100644 --- a/crates/gosub_render_pipeline/src/common/texture_store.rs +++ b/crates/gosub_render_pipeline/src/common/texture_store.rs @@ -24,12 +24,19 @@ impl TextureStore { } } - pub fn add(&mut self, width: usize, height: usize, data: Vec) -> TextureId { + pub fn add( + &mut self, + width: usize, + height: usize, + data: impl Into, + format: crate::render::backend::PixelFormat, + ) -> TextureId { let texture = Texture { id: self.next_id(), width, height, - data: std::sync::Arc::new(data), + data: data.into(), + format, }; let id = texture.id; diff --git a/crates/gosub_render_pipeline/src/render/backend.rs b/crates/gosub_render_pipeline/src/render/backend.rs index 96d5b2ae1..802fda739 100644 --- a/crates/gosub_render_pipeline/src/render/backend.rs +++ b/crates/gosub_render_pipeline/src/render/backend.rs @@ -48,12 +48,53 @@ pub enum PresentMode { Immediate, } -#[derive(Clone, Copy, Debug)] +/// In-memory byte order of a rasterized tile / pixel buffer. +/// +/// Both variants are premultiplied; they differ only in channel byte order, so +/// converting between them is a red/blue swap. A buffer is tagged with its format +/// at the point of production (the rasterizer) so consumers never have to assume an +/// order based on which backend feature happens to be compiled in. This matters +/// because Cargo feature unification (e.g. `cargo build --all`) can enable several +/// `backend_*` features at once, leaving a single rasterizer to win — its output +/// must be self-describing or colors silently swap. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PixelFormat { + /// Little-endian premultiplied ARGB32 — bytes are `[B, G, R, A]`. Produced by + /// the Cairo and Skia rasterizers (Cairo `Format::ARgb32`, Skia n32). PreMulArgb32, + /// Premultiplied RGBA8 — bytes are `[R, G, B, A]`. Produced by the Vello rasterizer. Rgba8, } +impl PixelFormat { + /// Returns `data` with bytes in `[R, G, B, A]` order, copying (and swapping R/B) + /// only when the source is not already RGBA. + pub fn to_rgba<'a>(self, data: &'a [u8]) -> std::borrow::Cow<'a, [u8]> { + match self { + PixelFormat::Rgba8 => std::borrow::Cow::Borrowed(data), + PixelFormat::PreMulArgb32 => std::borrow::Cow::Owned(swap_rb(data)), + } + } + + /// Returns `data` with bytes in `[B, G, R, A]` order (little-endian ARGB32), + /// copying (and swapping R/B) only when the source is not already in that order. + pub fn to_argb32<'a>(self, data: &'a [u8]) -> std::borrow::Cow<'a, [u8]> { + match self { + PixelFormat::PreMulArgb32 => std::borrow::Cow::Borrowed(data), + PixelFormat::Rgba8 => std::borrow::Cow::Owned(swap_rb(data)), + } + } +} + +/// Swap the red and blue channels of a tightly-packed 4-bytes-per-pixel buffer. +fn swap_rb(data: &[u8]) -> Vec { + let mut out = data.to_vec(); + for px in out.chunks_exact_mut(4) { + px.swap(0, 2); + } + out +} + #[derive(Clone, Copy, Debug)] pub enum GpuPixelFormat { Bgra8UnormSrgb, @@ -64,14 +105,16 @@ pub enum GpuPixelFormat { pub struct WgpuTextureId(pub u64); /// A single pre-rasterized tile for direct compositing in the host draw callback. -/// Pixel data is reference-counted so handing out a handle is zero-copy. +/// Pixel data is reference-counted (`Bytes`) so handing out a handle is zero-copy. #[derive(Clone, Debug)] pub struct CachedTile { pub page_x: f32, pub page_y: f32, pub width: u32, pub height: u32, - pub data: Arc>, + pub data: bytes::Bytes, + /// In-memory byte order of `data`, set by the rasterizer that produced it. + pub format: PixelFormat, } /// Safety: `ExternalHandle` can be sent between threads, but not shared. diff --git a/crates/gosub_render_pipeline/src/render/backends/cairo.rs b/crates/gosub_render_pipeline/src/render/backends/cairo.rs index 31b213749..6ca470ddd 100644 --- a/crates/gosub_render_pipeline/src/render/backends/cairo.rs +++ b/crates/gosub_render_pipeline/src/render/backends/cairo.rs @@ -82,7 +82,14 @@ impl RenderBackend for CairoBackend { cr.move_to(*x as f64, *y as f64); _ = cr.show_text(text); } - DisplayItem::Blit { x, y, w, h, data } => { + DisplayItem::Blit { + x, + y, + w, + h, + data, + format, + } => { let stride = (*w * 4) as i32; let expected_len = (*h as usize) * (stride as usize); if data.len() < expected_len { @@ -93,8 +100,11 @@ impl RenderBackend for CairoBackend { ); continue; } - // SAFETY: `data` is borrowed from the cached RenderList for the - // duration of this closure; Cairo reads (never writes) source data. + // Cairo's ARgb32 wants [B, G, R, A]; convert (no-op when the tile is already + // in that order) so colors are correct whatever rasterizer produced the tile. + let data = format.to_argb32(data); + // SAFETY: `data` is borrowed for the duration of this closure; + // Cairo reads (never writes) source data. let img_surface = unsafe { cairo::ImageSurface::create_for_data_unsafe( data.as_ptr() as *mut u8, diff --git a/crates/gosub_render_pipeline/src/render/backends/skia.rs b/crates/gosub_render_pipeline/src/render/backends/skia.rs index 7125ee5de..14ed10315 100644 --- a/crates/gosub_render_pipeline/src/render/backends/skia.rs +++ b/crates/gosub_render_pipeline/src/render/backends/skia.rs @@ -76,13 +76,22 @@ impl RenderBackend for SkiaBackend { paint.set_anti_alias(true); canvas.draw_str(text.as_str(), (*x, *y), &font, &paint); } - DisplayItem::Blit { x, y, w, h, data } => { + DisplayItem::Blit { + x, + y, + w, + h, + data, + format, + } => { let stride = (*w * 4) as usize; let expected = *h as usize * stride; if data.len() < expected { log::warn!("SkiaBackend: Blit data too short ({} < {})", data.len(), expected); continue; } + // BGRA8888 wants [B, G, R, A]; convert (no-op when already in that order). + let data = format.to_argb32(data); let info = skia_safe::ImageInfo::new( skia_safe::ISize::new(*w as i32, *h as i32), skia_safe::ColorType::BGRA8888, @@ -90,7 +99,7 @@ impl RenderBackend for SkiaBackend { None, ); if let Some(image) = - skia_safe::images::raster_from_data(&info, skia_safe::Data::new_copy(data), stride) + skia_safe::images::raster_from_data(&info, skia_safe::Data::new_copy(&data), stride) { canvas.draw_image(&image, (*x, *y), None); } diff --git a/crates/gosub_render_pipeline/src/render/backends/skia_gpu.rs b/crates/gosub_render_pipeline/src/render/backends/skia_gpu.rs index 843961f1f..e39b56429 100644 --- a/crates/gosub_render_pipeline/src/render/backends/skia_gpu.rs +++ b/crates/gosub_render_pipeline/src/render/backends/skia_gpu.rs @@ -125,12 +125,21 @@ impl RenderBackend for SkiaGpuBack paint.set_anti_alias(true); canvas.draw_str(text.as_str(), (*x, *y), &font, &paint); } - DisplayItem::Blit { x, y, w, h, data } => { + DisplayItem::Blit { + x, + y, + w, + h, + data, + format, + } => { let stride = (*w * 4) as usize; if data.len() < *h as usize * stride { log::warn!("SkiaGpuBackend: Blit data too short"); continue; } + // BGRA8888 wants [B, G, R, A]; convert (no-op when already in that order). + let data = format.to_argb32(data); let info = ImageInfo::new( (*w as i32, *h as i32), skia_safe::ColorType::BGRA8888, @@ -138,7 +147,7 @@ impl RenderBackend for SkiaGpuBack None, ); if let Some(image) = - skia_safe::images::raster_from_data(&info, skia_safe::Data::new_copy(data), stride) + skia_safe::images::raster_from_data(&info, skia_safe::Data::new_copy(&data), stride) { canvas.draw_image(&image, (*x, *y), None); } diff --git a/crates/gosub_render_pipeline/src/render/backends/vello.rs b/crates/gosub_render_pipeline/src/render/backends/vello.rs index 7b2e34dc8..c19ee48f4 100644 --- a/crates/gosub_render_pipeline/src/render/backends/vello.rs +++ b/crates/gosub_render_pipeline/src/render/backends/vello.rs @@ -167,16 +167,18 @@ impl VelloBackend { (*color).into(), ); } - DisplayItem::Blit { x, y, w, h, data } => { - // Cairo emits premultiplied ARgb32; in-memory bytes are [B, G, R, A] on - // little-endian. peniko ImageFormat::Rgba8 expects [R, G, B, A], so swap. - let mut rgba = vec![0u8; data.len()]; - for (dst, src) in rgba.chunks_exact_mut(4).zip(data.chunks_exact(4)) { - dst[0] = src[2]; - dst[1] = src[1]; - dst[2] = src[0]; - dst[3] = src[3]; - } + DisplayItem::Blit { + x, + y, + w, + h, + data, + format, + } => { + // peniko ImageFormat::Rgba8 expects [R, G, B, A]. The tile may be premultiplied + // ARGB32 (Cairo/Skia, [B, G, R, A]) or already RGBA (Vello); `to_rgba` swaps only + // when needed, so colors are correct regardless of which rasterizer produced it. + let rgba = format.to_rgba(data).into_owned(); let blob = vello::peniko::Blob::::new(Arc::new(rgba)); let image = ImageData { data: blob, diff --git a/crates/gosub_render_pipeline/src/render/render_list.rs b/crates/gosub_render_pipeline/src/render/render_list.rs index e93692a54..972c8dadb 100644 --- a/crates/gosub_render_pipeline/src/render/render_list.rs +++ b/crates/gosub_render_pipeline/src/render/render_list.rs @@ -166,7 +166,7 @@ pub enum DisplayItem { max_width: Option, }, - /// Blit a rasterized tile at `(x, y)`. Pixels are premultiplied ARgb32; + /// Blit a rasterized tile at `(x, y)`. Pixels are premultiplied; /// stride = `w * 4` bytes. Produced by the pipeline rasterizer. Blit { /// Top-left x position in page coordinates. @@ -177,8 +177,10 @@ pub enum DisplayItem { w: u32, /// Tile height in pixels. h: u32, - /// Raw ARgb32 pixel data (length = `h * w * 4`). Arc-shared from the rasterizer output. - data: std::sync::Arc>, + /// Raw premultiplied pixel data (length = `h * w * 4`). `Bytes`-shared from the rasterizer output. + data: bytes::Bytes, + /// In-memory byte order of `data`, set by the rasterizer that produced it. + format: crate::render::backend::PixelFormat, }, } diff --git a/crates/gosub_renderer_cairo/src/compositor/inner.rs b/crates/gosub_renderer_cairo/src/compositor/inner.rs index eff457af0..b2e5e3355 100644 --- a/crates/gosub_renderer_cairo/src/compositor/inner.rs +++ b/crates/gosub_renderer_cairo/src/compositor/inner.rs @@ -41,7 +41,7 @@ pub fn compose_layer(cr: &cairo::Context, layer_id: LayerId, state: &BrowserStat }; let surface = match ImageSurface::create_for_data( - (*texture.data).clone(), + texture.data.to_vec(), cairo::Format::ARgb32, texture.width as i32, texture.height as i32, diff --git a/crates/gosub_renderer_cairo/src/rasterizer.rs b/crates/gosub_renderer_cairo/src/rasterizer.rs index 8b0b188e8..0dc8b8714 100644 --- a/crates/gosub_renderer_cairo/src/rasterizer.rs +++ b/crates/gosub_renderer_cairo/src/rasterizer.rs @@ -133,7 +133,12 @@ impl Rasterable for CairoRasterizer { return None; }; - let texture_id = texture_store.add(w, h, data.to_vec()); + let texture_id = texture_store.add( + w, + h, + data.to_vec(), + gosub_render_pipeline::render::backend::PixelFormat::PreMulArgb32, + ); Some(texture_id) } diff --git a/crates/gosub_renderer_skia/src/rasterizer.rs b/crates/gosub_renderer_skia/src/rasterizer.rs index 86c94cff2..481040d15 100644 --- a/crates/gosub_renderer_skia/src/rasterizer.rs +++ b/crates/gosub_renderer_skia/src/rasterizer.rs @@ -81,7 +81,12 @@ impl Rasterable for SkiaRasterizer { }; let pixels = bytes.to_vec(); - let texture_id = texture_store.add(width as usize, height as usize, pixels); + let texture_id = texture_store.add( + width as usize, + height as usize, + pixels, + gosub_render_pipeline::render::backend::PixelFormat::PreMulArgb32, + ); Some(texture_id) } diff --git a/crates/gosub_renderer_vello/src/compositor/inner.rs b/crates/gosub_renderer_vello/src/compositor/inner.rs index 15d429207..1ebdc49e4 100644 --- a/crates/gosub_renderer_vello/src/compositor/inner.rs +++ b/crates/gosub_renderer_vello/src/compositor/inner.rs @@ -47,8 +47,11 @@ pub fn compose_layer(scene: &mut vello::Scene, layer_id: LayerId, state: &Browse continue; }; + // peniko ImageFormat::Rgba8 expects [R, G, B, A]; convert from the tile's tagged + // byte order (no-op when the rasterizer already produced RGBA). + let rgba = texture.format.to_rgba(&texture.data).into_owned(); let surface = ImageData { - data: Blob::from(texture.data.as_ref().clone()), + data: Blob::from(rgba), format: ImageFormat::Rgba8, alpha_type: ImageAlphaType::AlphaPremultiplied, width: texture.width as u32, diff --git a/crates/gosub_renderer_vello/src/rasterizer.rs b/crates/gosub_renderer_vello/src/rasterizer.rs index bf3b2c873..9c65858d5 100644 --- a/crates/gosub_renderer_vello/src/rasterizer.rs +++ b/crates/gosub_renderer_vello/src/rasterizer.rs @@ -120,7 +120,12 @@ impl Rasterable for VelloRasterizer { tile.id, )?; - let texture_id = texture_store.add(tile_size.width as usize, tile_size.height as usize, texture_data); + let texture_id = texture_store.add( + tile_size.width as usize, + tile_size.height as usize, + texture_data, + gosub_render_pipeline::render::backend::PixelFormat::Rgba8, + ); Some(texture_id) } diff --git a/examples/egui-vello/main.rs b/examples/egui-vello/main.rs index 2fb4bef81..3bb27fa2b 100644 --- a/examples/egui-vello/main.rs +++ b/examples/egui-vello/main.rs @@ -309,8 +309,11 @@ impl BrowserApp { let dst_y0 = screen_y.max(0) as usize; let tw = tw as usize; let th = th as usize; + // Normalize to [R, G, B, A] regardless of which rasterizer produced the tile + // (Cargo feature unification may select Cairo's ARGB32 over Vello's RGBA). + let tile_data = tile.format.to_rgba(&tile.data); let tile_u32 = - unsafe { std::slice::from_raw_parts(tile.data.as_ptr() as *const u32, tile.data.len() / 4) }; + unsafe { std::slice::from_raw_parts(tile_data.as_ptr() as *const u32, tile_data.len() / 4) }; for tile_row in tile_start_row..th { let dst_y = dst_y0 + (tile_row - tile_start_row); if dst_y >= h { @@ -326,7 +329,7 @@ impl BrowserApp { } } - // Vello tiles are Rgba8Unorm (byte order R, G, B, A) — no channel swap needed. + // `buf` now holds [R, G, B, A] bytes (normalized per-tile above). let mut rgba = Vec::with_capacity(w * h * 4); for &px in &buf { let r = (px & 0xFF) as u8;