Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions bin/gosub-screenshot/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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 {
Expand Down
66 changes: 43 additions & 23 deletions crates/gosub_engine/src/engine/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ struct BakedTile {
page_y: f64,
width: u32,
height: u32,
data: Arc<Vec<u8>>,
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.
Expand All @@ -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<TileCacheKey, (u32, u32, Arc<Vec<u8>>)>,
tile_pixel_cache: std::collections::HashMap<TileCacheKey, (u32, u32, bytes::Bytes)>,
}

/// BrowsingContext dedicated to a specific tab
Expand Down Expand Up @@ -932,7 +934,7 @@ fn pipeline_build_cache(
#[cfg(feature = "backend_vello")] _vello_resources: Option<
std::sync::Arc<gosub_render_pipeline::render::backends::vello::WgpuResources>,
>,
prev_tile_cache: std::collections::HashMap<TileCacheKey, (u32, u32, Arc<Vec<u8>>)>,
prev_tile_cache: std::collections::HashMap<TileCacheKey, (u32, u32, bytes::Bytes)>,
) -> PipelineCache {
use gosub_render_pipeline::common::browser_state::{BrowserState, WireframeState};
use gosub_render_pipeline::common::document::pipeline_doc::GosubDocumentAdapter;
Expand Down Expand Up @@ -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();
Expand All @@ -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<BakedTile>, Option<new_cache_entry>)
type CacheEntry = (TileCacheKey, (u32, u32, Arc<Vec<u8>>));
type CacheEntry = (TileCacheKey, (u32, u32, bytes::Bytes));
let results: Vec<(TileId, Option<BakedTile>, Option<CacheEntry>)> = dirty_ids
.par_iter()
.map(|&tile_id| {
Expand All @@ -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);
}
Expand All @@ -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();
Expand All @@ -1148,7 +1156,7 @@ fn pipeline_build_cache(
let mut cache_hits = 0usize;
let mut empty = 0usize;
let mut tiles: Vec<BakedTile> = Vec::with_capacity(results.len());
let mut new_tile_cache: std::collections::HashMap<TileCacheKey, (u32, u32, Arc<Vec<u8>>)> =
let mut new_tile_cache: std::collections::HashMap<TileCacheKey, (u32, u32, bytes::Bytes)> =
std::collections::HashMap::with_capacity(results.len());

for (tile_id, baked, cache_entry) in results {
Expand Down Expand Up @@ -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,
});
}
}
Expand All @@ -1278,13 +1287,13 @@ fn pipeline_build_cache(
not(feature = "backend_cairo"),
not(feature = "backend_skia")
))]
let new_tile_cache: std::collections::HashMap<TileCacheKey, (u32, u32, Arc<Vec<u8>>)> =
let new_tile_cache: std::collections::HashMap<TileCacheKey, (u32, u32, bytes::Bytes)> =
std::collections::HashMap::new();

#[cfg(not(any(feature = "backend_cairo", feature = "backend_skia", feature = "backend_vello")))]
let baked_tiles: Vec<BakedTile> = Vec::new();
#[cfg(not(any(feature = "backend_cairo", feature = "backend_skia", feature = "backend_vello")))]
let new_tile_cache: std::collections::HashMap<TileCacheKey, (u32, u32, Arc<Vec<u8>>)> =
let new_tile_cache: std::collections::HashMap<TileCacheKey, (u32, u32, bytes::Bytes)> =
std::collections::HashMap::new();

timing_stop!(ts_total);
Expand All @@ -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::<Vec<_>>(),
);
Expand Down Expand Up @@ -1334,7 +1344,7 @@ fn pipeline_hover_repaint(
#[cfg(feature = "backend_vello")] _vello_resources: Option<
std::sync::Arc<gosub_render_pipeline::render::backends::vello::WgpuResources>,
>,
prev_tile_cache: std::collections::HashMap<TileCacheKey, (u32, u32, Arc<Vec<u8>>)>,
prev_tile_cache: std::collections::HashMap<TileCacheKey, (u32, u32, bytes::Bytes)>,
) -> PipelineCache {
use gosub_render_pipeline::common::browser_state::{BrowserState, WireframeState};
use gosub_render_pipeline::common::geo::{Dimension as PipelineDimension, Rect as PipelineRect};
Expand Down Expand Up @@ -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::<Vec<_>>(),
);
Expand Down Expand Up @@ -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();
Expand All @@ -1524,7 +1539,7 @@ fn pipeline_hover_repaint(
layer_ids.len()
);

type CacheEntry = (TileCacheKey, (u32, u32, Arc<Vec<u8>>));
type CacheEntry = (TileCacheKey, (u32, u32, bytes::Bytes));
let results: Vec<(TileId, Option<BakedTile>, Option<CacheEntry>)> = dirty_ids
.par_iter()
.map(|&tile_id| {
Expand All @@ -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,
);
Expand All @@ -1554,17 +1570,18 @@ 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();

let mut rasterized = 0usize;
let mut cache_hits = 0usize;
let mut tiles: Vec<BakedTile> = Vec::with_capacity(results.len());
let mut new_tile_cache: std::collections::HashMap<TileCacheKey, (u32, u32, Arc<Vec<u8>>)> =
let mut new_tile_cache: std::collections::HashMap<TileCacheKey, (u32, u32, bytes::Bytes)> =
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) {
Expand Down Expand Up @@ -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,
});
}
}
Expand All @@ -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<BakedTile>,
std::collections::HashMap<TileCacheKey, (u32, u32, Arc<Vec<u8>>)>,
std::collections::HashMap<TileCacheKey, (u32, u32, bytes::Bytes)>,
) = (Vec::new(), std::collections::HashMap::new());

// Merge: newly rasterized hover tiles + clean tiles carried from previous render.
Expand All @@ -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::<Vec<_>>(),
);
Expand Down Expand Up @@ -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;
}
Expand Down
8 changes: 5 additions & 3 deletions crates/gosub_render_pipeline/src/common/texture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>>,
pub data: bytes::Bytes,
/// In-memory byte order of `data`, set by the rasterizer that produced it.
pub format: crate::render::backend::PixelFormat,
}
11 changes: 9 additions & 2 deletions crates/gosub_render_pipeline/src/common/texture_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,19 @@ impl TextureStore {
}
}

pub fn add(&mut self, width: usize, height: usize, data: Vec<u8>) -> TextureId {
pub fn add(
&mut self,
width: usize,
height: usize,
data: impl Into<bytes::Bytes>,
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;
Expand Down
49 changes: 46 additions & 3 deletions crates/gosub_render_pipeline/src/render/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
}
}
}

Comment on lines +72 to +88

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we can do this more efficiently by passing in a &mut [u8] and swapping it in place

/// Swap the red and blue channels of a tightly-packed 4-bytes-per-pixel buffer.
fn swap_rb(data: &[u8]) -> Vec<u8> {
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,
Expand All @@ -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<Vec<u8>>,
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.
Expand Down
Loading
Loading