Skip to content
Merged
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
931 changes: 915 additions & 16 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@
pkgs.pkg-config
pkgs.mold
pkgs.fontconfig
pkgs.vulkan-loader
pkgs.vulkan-tools
];

LD_LIBRARY_PATH = pkgs.lib.optionalString pkgs.stdenv.isLinux (
pkgs.lib.makeLibraryPath [ pkgs.vulkan-loader ]
);
};
}
);
Expand Down
14 changes: 4 additions & 10 deletions tellur-core/src/composite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
//! source pixels skip the write entirely and fully-opaque ones go
//! through a 4-byte copy.

use crate::raster::{PixelFormat, RasterImage, Resolution};
use crate::raster::{CpuRasterImage, PixelFormat, Resolution};

/// Source-over composites `src` onto `dst` at pixel offset
/// `(offset_x, offset_y)`. Both buffers hold 8-bit straight-alpha RGBA
Expand All @@ -28,7 +28,7 @@ use crate::raster::{PixelFormat, RasterImage, Resolution};
pub fn composite_at(
dst: &mut [u8],
dst_size: Resolution,
src: &RasterImage,
src: &CpuRasterImage,
offset_x: i32,
offset_y: i32,
) {
Expand Down Expand Up @@ -130,16 +130,10 @@ fn blend_row(dst: &mut [u8], src: &[u8]) {
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;

fn image(width: u32, height: u32, pixels: Vec<u8>) -> RasterImage {
fn image(width: u32, height: u32, pixels: Vec<u8>) -> CpuRasterImage {
assert_eq!(pixels.len(), (width * height * 4) as usize);
RasterImage {
width,
height,
format: PixelFormat::Rgba8,
pixels: Bytes::from(pixels),
}
CpuRasterImage::new(width, height, PixelFormat::Rgba8, pixels)
}

/// Straight-alpha Porter-Duff source-over carried out in `f64`, used
Expand Down
54 changes: 42 additions & 12 deletions tellur-core/src/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,11 @@
//! source-over compositing it onto the output at the corresponding pixel
//! offset.

use bytes::Bytes;

use crate::composite::composite_at;
use crate::geometry::{Constraints, Rect, Transform, Vec2};
use crate::placement::Placed;
use crate::raster::{PixelFormat, RasterComponent, RasterImage, Resolution};
use crate::render_context::RenderContext;
use crate::render_context::{CompositeInput, RenderContext};
use crate::vector::{Group, Node, VectorComponent, VectorGraphic};

#[derive(PartialEq, Hash)]
Expand Down Expand Up @@ -247,12 +245,48 @@ pub(crate) fn composite_children(
placed: &[(Vec2, Vec2, &dyn RasterComponent)],
ctx: &mut dyn RenderContext,
) -> RasterImage {
let pixel_count = (target.width as usize) * (target.height as usize);
let mut accum = vec![0u8; pixel_count * 4];

let scale_x = target.width as f32 / paint_rect.size.0;
let scale_y = target.height as f32 / paint_rect.size.1;
let gpu_available = ctx.prefers_gpu() && ctx.gpu_backend().is_some();

if gpu_available {
let mut rendered = Vec::with_capacity(placed.len());
for (position, child_size, child) in placed {
let bounds = child.paint_bounds(*child_size);
let child_px_w = (bounds.size.0 * scale_x).round().max(1.0) as u32;
let child_px_h = (bounds.size.1 * scale_y).round().max(1.0) as u32;
let paint_x = position.0 + bounds.origin.0 - paint_rect.origin.0;
let paint_y = position.1 + bounds.origin.1 - paint_rect.origin.1;
let offset_x = (paint_x * scale_x).round() as i32;
let offset_y = (paint_y * scale_y).round() as i32;
let image = ctx.render(*child, *child_size, Resolution::new(child_px_w, child_px_h));
rendered.push((image, offset_x, offset_y));
}

let inputs: Vec<CompositeInput<'_>> = rendered
.iter()
.map(|(image, offset_x, offset_y)| CompositeInput {
image,
offset_x: *offset_x,
offset_y: *offset_y,
})
.collect();
if let Some(gpu) = ctx.gpu_backend() {
if let Some(image) = gpu.composite(target, &inputs) {
return image;
}
}

let mut accum = vec![0u8; (target.width as usize) * (target.height as usize) * 4];
for (image, offset_x, offset_y) in rendered {
let image = ctx.readback(image);
composite_at(&mut accum, target, &image, offset_x, offset_y);
}

return RasterImage::cpu(target.width, target.height, PixelFormat::Rgba8, accum);
}

let mut accum = vec![0u8; (target.width as usize) * (target.height as usize) * 4];
for (position, child_size, child) in placed {
let bounds = child.paint_bounds(*child_size);
let child_px_w = (bounds.size.0 * scale_x).round().max(1.0) as u32;
Expand All @@ -265,15 +299,11 @@ pub(crate) fn composite_children(
// Route the child render through the context so cache lookups
// can intercept it before the underlying `render` runs.
let image = ctx.render(*child, *child_size, Resolution::new(child_px_w, child_px_h));
let image = ctx.readback(image);
composite_at(&mut accum, target, &image, offset_x, offset_y);
}

RasterImage {
width: target.width,
height: target.height,
format: PixelFormat::Rgba8,
pixels: Bytes::from(accum),
}
RasterImage::cpu(target.width, target.height, PixelFormat::Rgba8, accum)
}

/// Smallest axis-aligned rectangle containing both `a` and `b`.
Expand Down
21 changes: 7 additions & 14 deletions tellur-core/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -663,8 +663,6 @@ pub mod raster {
//! Raster equivalents of the vector layout containers. Same shape
//! and semantics; operate on `Box<dyn RasterComponent>`.

use bytes::Bytes;

use std::hash::{Hash, Hasher};

use super::{
Expand Down Expand Up @@ -1097,12 +1095,12 @@ pub mod raster {
_ctx: &mut dyn RenderContext,
) -> RasterImage {
let bytes = (target.width as usize) * (target.height as usize) * 4;
RasterImage {
width: target.width,
height: target.height,
format: PixelFormat::Rgba8,
pixels: Bytes::from(vec![0u8; bytes]),
}
RasterImage::cpu(
target.width,
target.height,
PixelFormat::Rgba8,
vec![0u8; bytes],
)
}
}

Expand Down Expand Up @@ -1136,12 +1134,7 @@ pub mod raster {
buf.push(b);
buf.push(a);
}
RasterImage {
width: target.width,
height: target.height,
format: PixelFormat::Rgba8,
pixels: Bytes::from(buf),
}
RasterImage::cpu(target.width, target.height, PixelFormat::Rgba8, buf)
}
}

Expand Down
154 changes: 152 additions & 2 deletions tellur-core/src/raster.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::any::Any;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::io::Write;
use std::sync::Arc;

use bytes::Bytes;
use thiserror::Error;
Expand All @@ -10,13 +12,136 @@ use crate::geometry::{Constraints, Rect, Vec2};
use crate::render_context::RenderContext;

#[derive(Debug, Clone)]
pub struct RasterImage {
pub enum RasterImage {
Cpu(CpuRasterImage),
Gpu(GpuSurface),
}

impl RasterImage {
pub fn cpu(width: u32, height: u32, format: PixelFormat, pixels: impl Into<Bytes>) -> Self {
Self::Cpu(CpuRasterImage {
width,
height,
format,
pixels: pixels.into(),
})
}

pub fn width(&self) -> u32 {
match self {
Self::Cpu(image) => image.width,
Self::Gpu(surface) => surface.width,
}
}

pub fn height(&self) -> u32 {
match self {
Self::Cpu(image) => image.height,
Self::Gpu(surface) => surface.height,
}
}

pub fn format(&self) -> PixelFormat {
match self {
Self::Cpu(image) => image.format,
Self::Gpu(surface) => surface.format,
}
}

pub fn as_cpu(&self) -> Option<&CpuRasterImage> {
match self {
Self::Cpu(image) => Some(image),
Self::Gpu(_) => None,
}
}

pub fn into_cpu(self) -> Result<CpuRasterImage, Self> {
match self {
Self::Cpu(image) => Ok(image),
Self::Gpu(_) => Err(self),
}
}
}

impl From<CpuRasterImage> for RasterImage {
fn from(image: CpuRasterImage) -> Self {
Self::Cpu(image)
}
}

impl From<GpuSurface> for RasterImage {
fn from(surface: GpuSurface) -> Self {
Self::Gpu(surface)
}
}

#[derive(Debug, Clone)]
pub struct CpuRasterImage {
pub width: u32,
pub height: u32,
pub format: PixelFormat,
pub pixels: Bytes,
}

/// Backend-owned GPU image handle.
///
/// `tellur-core` deliberately keeps this opaque: concrete backends can store a
/// `wgpu::Texture`, texture view, command-graph node, or another device-local
/// handle behind the `Arc<dyn Any>`, while core remains dependency-free.
#[derive(Clone)]
pub struct GpuSurface {
pub width: u32,
pub height: u32,
pub format: PixelFormat,
backend: &'static str,
handle: Arc<dyn Any + Send + Sync>,
}

impl GpuSurface {
pub fn new(
width: u32,
height: u32,
format: PixelFormat,
backend: &'static str,
handle: Arc<dyn Any + Send + Sync>,
) -> Self {
Self {
width,
height,
format,
backend,
handle,
}
}

pub fn backend(&self) -> &'static str {
self.backend
}

pub fn handle(&self) -> &(dyn Any + Send + Sync) {
self.handle.as_ref()
}

pub fn handle_arc(&self) -> Arc<dyn Any + Send + Sync> {
Arc::clone(&self.handle)
}

pub fn downcast_handle<T: Any + Send + Sync>(&self) -> Option<&T> {
self.handle.as_ref().downcast_ref::<T>()
}
}

impl fmt::Debug for GpuSurface {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GpuSurface")
.field("width", &self.width)
.field("height", &self.height)
.field("format", &self.format)
.field("backend", &self.backend)
.finish_non_exhaustive()
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PixelFormat {
/// 8-bit per channel sRGB with straight (non-premultiplied) alpha.
Expand Down Expand Up @@ -111,11 +236,22 @@ pub enum PngExportError {
UnsupportedFormat(PixelFormat),
#[error("pixel buffer size mismatch: expected {expected} bytes, got {actual}")]
SizeMismatch { expected: usize, actual: usize },
#[error("PNG export requires a CPU image; got GPU surface from backend {backend}")]
GpuSurface { backend: &'static str },
#[error("PNG encoding failed: {0}")]
Encode(#[from] png::EncodingError),
}

impl RasterImage {
impl CpuRasterImage {
pub fn new(width: u32, height: u32, format: PixelFormat, pixels: impl Into<Bytes>) -> Self {
Self {
width,
height,
format,
pixels: pixels.into(),
}
}

/// Encodes the image as PNG and writes it to `writer`.
///
/// Only `PixelFormat::Rgba8` is currently supported. HDR formats require
Expand All @@ -141,3 +277,17 @@ impl RasterImage {
Ok(())
}
}

impl RasterImage {
/// Encodes a CPU image as PNG and writes it to `writer`.
///
/// GPU images must be read back through the active render context first.
pub fn export_png<W: Write>(&self, writer: W) -> Result<(), PngExportError> {
match self {
Self::Cpu(image) => image.export_png(writer),
Self::Gpu(surface) => Err(PngExportError::GpuSurface {
backend: surface.backend(),
}),
}
}
}
Loading
Loading