mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-04 11:05:56 +00:00
Render: pictures drawn tile by tile into any pixel window (#5624, W17b S1)
The CPU product renderer, reshaped so drawings can use it: one rasteriser (`Raster`) prepares a scene once (appearances, projected triangles, glass sorted back to front), then draws each tile of the picture on a canvas of its own. A sample's position comes from its whole-picture index, so tiles put side by side are the picture drawn whole, to the byte. - `ImageTask` (render.image) is that rasteriser over one whole-picture tile from the product camera, then its PNG: the same output, pinned by the previous commit's rows. - `PictureTask` draws a `PictureRequest`: a `Projection` (the product camera, or an `OrthoFrame`, W17a's view frame as plain data: a `Plane` basis, a scale and a paper shift, on a `geom::pixel_grid::PixelGrid`), any list of `PixelWindow` tiles, `Supersample::One` (pixel centres) or `Two` (i + 1/4 and i + 3/4), a `BodyRole` per body (`DepthOnly` hides what lies behind it and draws nothing itself) and an optional background (none: transparent). Each tile comes back as straight-alpha sRGB RGBA rows, blended in linear light. - The orthographic map is the frame's paper map, onto the grid, so lines drawn from the same frame register with the pixels (to 1e-9 mm in the test). - Limits: 65,536 px a side, no total cap; each tile under the resolver's `RenderLimits`, as the product image is; at most 65,536 tiles; the prepared lists as a product image's; pixel tests up to a fixed 400 M a picture (128 M for a product image, as before), counted at admission by the one projection the raster uses, as main counts a product image's, and only where a triangle meets the tiles asked for. Not here: the drawing light, stored per-tile bins (S2, measured), the paper scene's picture item (S3), 4x sampling, and `raster::render`, the snapshot rasteriser, which stays as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+576
-173
@@ -1,12 +1,18 @@
|
||||
//! Incremental product rasterization. Every phase yields between bounded work units.
|
||||
//!
|
||||
//! One rasteriser draws both the product image ([`ImageTask`]: the product camera, one tile, then
|
||||
//! a PNG) and pictures ([`PictureTask`]: the product camera or an orthographic view frame, any
|
||||
//! pixel windows, each handed back as straight-alpha sRGB rows). It prepares once (appearances,
|
||||
//! projected triangles, glass sorted back to front) and then draws each tile.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::Serialize;
|
||||
use solidcraft_geom::Vec3;
|
||||
use solidcraft_geom::pixel_grid::{MAX_SIDE_PX, PixelGrid};
|
||||
use solidcraft_geom::{Plane, Vec2, Vec3};
|
||||
|
||||
use super::{CancelToken, ErrorCode, ImageRequest, JobError, Phase, Progress, Result};
|
||||
use crate::{FaceAppearance, Mat4, ProductScene, RenderFormat, RenderMode, RenderOperation, RenderRequest};
|
||||
use crate::{Camera, FaceAppearance, Mat4, ProductScene, RenderFormat, RenderMode, RenderOperation, RenderRequest};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "image_tests.rs"]
|
||||
@@ -16,8 +22,9 @@ pub const MAX_TRIANGLES: u64 = 262_144;
|
||||
pub const MAX_GEOMETRY_BYTES: u64 = 128 * 1024 * 1024;
|
||||
pub const MAX_JOB_BYTES: u64 = 384 * 1024 * 1024;
|
||||
pub const MAX_PIXEL_TESTS: u64 = 128_000_000;
|
||||
/// A picture's, however large: a fixed bound on its time, about a minute at worst on one thread.
|
||||
pub const MAX_PICTURE_PIXEL_TESTS: u64 = 400_000_000;
|
||||
const MAX_STEP: usize = 16_384;
|
||||
const SS: usize = 2;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
pub struct Admission {
|
||||
@@ -83,7 +90,10 @@ pub fn admit(scene: &ProductScene, request: &ImageRequest) -> Result<Admission>
|
||||
|
||||
pub(crate) fn admit_base(scene: &ProductScene, request: &ImageRequest) -> Result<Admission> {
|
||||
let mut admission = admit_raster(admit_source(scene, request, MAX_TRIANGLES)?)?;
|
||||
admission.projected_pixel_tests = Some(projected_pixel_tests(scene, request)?);
|
||||
let projection = Projection::Camera { camera: request.camera, width: request.width, height: request.height };
|
||||
let view = projection.view(scene, Supersample::Two.factor())?;
|
||||
let whole = tile_samples(&[PixelWindow { x0: 0, y0: 0, x1: request.width, y1: request.height }], Supersample::Two.factor());
|
||||
admission.projected_pixel_tests = Some(projected_pixel_tests(scene, &view, &[], whole, MAX_PIXEL_TESTS)?);
|
||||
Ok(admission)
|
||||
}
|
||||
|
||||
@@ -130,8 +140,16 @@ pub(crate) fn admit_raytrace_source(scene: &ProductScene, request: &ImageRequest
|
||||
}
|
||||
|
||||
fn admit_source_with_budget(scene: &ProductScene, request: &ImageRequest, max_triangles: u64, budget: super::CpuBudget) -> Result<Source> {
|
||||
validate_request(request)?;
|
||||
let limits = budget.limits();
|
||||
admit_source_with_limits(scene, request, max_triangles, limits.max_bodies, limits.max_geometry_bytes, budget.image_limits(false))
|
||||
admit_source_with_limits(
|
||||
scene,
|
||||
(request.width, request.height),
|
||||
max_triangles,
|
||||
limits.max_bodies,
|
||||
limits.max_geometry_bytes,
|
||||
budget.image_limits(false),
|
||||
)
|
||||
}
|
||||
|
||||
/// A registered executor may request a larger finite source envelope without
|
||||
@@ -156,17 +174,12 @@ pub fn admit_geometry_with_limits(
|
||||
{
|
||||
return Err(limit("registered source envelope exceeds implementation bounds"));
|
||||
}
|
||||
Ok(admit_source_with_limits(scene, request, max_triangles, max_bodies, max_geometry_bytes, image_limits)?.admission)
|
||||
validate_request(request)?;
|
||||
Ok(admit_source_with_limits(scene, (request.width, request.height), max_triangles, max_bodies, max_geometry_bytes, image_limits)?.admission)
|
||||
}
|
||||
|
||||
fn admit_source_with_limits(
|
||||
scene: &ProductScene,
|
||||
request: &ImageRequest,
|
||||
max_triangles: u64,
|
||||
max_bodies: usize,
|
||||
max_geometry_bytes: u64,
|
||||
image_limits: crate::RenderLimits,
|
||||
) -> Result<Source> {
|
||||
/// What an image request must be, whichever executor runs it.
|
||||
fn validate_request(request: &ImageRequest) -> Result<()> {
|
||||
if !request.camera.is_valid() {
|
||||
return Err(JobError::new(ErrorCode::InvalidRequest, "invalid image camera"));
|
||||
}
|
||||
@@ -176,6 +189,18 @@ fn admit_source_with_limits(
|
||||
if request.integrator == super::Integrator::Raytrace && request.studio_weights.is_some() {
|
||||
return Err(JobError::new(ErrorCode::InvalidRequest, "CPU ray tracing uses explicit lights rather than studio_weights"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `(width, height)`: the image (or tile) size whose pixel scratch is estimated.
|
||||
fn admit_source_with_limits(
|
||||
scene: &ProductScene,
|
||||
(width, height): (u32, u32),
|
||||
max_triangles: u64,
|
||||
max_bodies: usize,
|
||||
max_geometry_bytes: u64,
|
||||
image_limits: crate::RenderLimits,
|
||||
) -> Result<Source> {
|
||||
if scene.meshes.len() > max_bodies {
|
||||
return Err(limit("too many product bodies"));
|
||||
}
|
||||
@@ -189,8 +214,8 @@ fn admit_source_with_limits(
|
||||
backend: None,
|
||||
mode: RenderMode::Preview,
|
||||
operation: RenderOperation::Image,
|
||||
width: u64::from(request.width),
|
||||
height: u64::from(request.height),
|
||||
width: u64::from(width),
|
||||
height: u64::from(height),
|
||||
format: Some(RenderFormat::Png),
|
||||
features: Vec::new(),
|
||||
policy: Default::default(),
|
||||
@@ -231,7 +256,7 @@ fn admit_source_with_limits(
|
||||
return Err(invalid("invalid body opacity"));
|
||||
}
|
||||
}
|
||||
let encoded_limit = u64::from(request.width) * u64::from(request.height) * 8 + 65_536;
|
||||
let encoded_limit = u64::from(width) * u64::from(height) * 8 + 65_536;
|
||||
Ok(Source {
|
||||
admission: Admission {
|
||||
triangles,
|
||||
@@ -272,26 +297,278 @@ fn admit_raster(source: Source) -> Result<Admission> {
|
||||
Ok(a)
|
||||
}
|
||||
|
||||
/// The same clipping, supersampling and bounds govern admission and raster work.
|
||||
struct Projection {
|
||||
fn add_pixel_tests(sum: &mut u64, triangle: &Triangle, within: [usize; 4], max: u64) -> Result<()> {
|
||||
let Some([x0, x1, y0, y1]) = narrowed(triangle.bounds, within) else {
|
||||
return Ok(());
|
||||
};
|
||||
let tests = ((x1 - x0 + 1) as u64).checked_mul((y1 - y0 + 1) as u64).ok_or_else(|| limit("pixel work overflow"))?;
|
||||
*sum = sum.checked_add(tests).ok_or_else(|| limit("pixel work overflow"))?;
|
||||
if *sum > max {
|
||||
return Err(limit(&format!("projected product exceeds {max} pixel tests; reduce image dimensions or visible geometry")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_face(face: &FaceAppearance) -> Result<()> {
|
||||
if !face.opacity.is_finite() || !(0.0..=1.0).contains(&face.opacity) {
|
||||
return Err(invalid("invalid face opacity"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Where sample bounds `[x0, x1, y0, y1]` (inclusive) meet `within`, if they do.
|
||||
fn narrowed([x0, x1, y0, y1]: [usize; 4], [from_x, to_x, from_y, to_y]: [usize; 4]) -> Option<[usize; 4]> {
|
||||
let bounds = [x0.max(from_x), x1.min(to_x), y0.max(from_y), y1.min(to_y)];
|
||||
(bounds[0] <= bounds[1] && bounds[2] <= bounds[3]).then_some(bounds)
|
||||
}
|
||||
|
||||
/// The sample bounds round `tiles`, each non-empty, at `ss` samples a pixel each way: only these are
|
||||
/// drawn, so only these are counted, however large the canvas is.
|
||||
fn tile_samples(tiles: &[PixelWindow], ss: usize) -> [usize; 4] {
|
||||
let (x0, x1) = (tiles.iter().map(|t| t.x0).min().unwrap_or(0), tiles.iter().map(|t| t.x1).max().unwrap_or(1));
|
||||
let (y0, y1) = (tiles.iter().map(|t| t.y0).min().unwrap_or(0), tiles.iter().map(|t| t.y1).max().unwrap_or(1));
|
||||
[x0 as usize * ss, x1 as usize * ss - 1, y0 as usize * ss, y1 as usize * ss - 1]
|
||||
}
|
||||
|
||||
fn projected_pixel_tests(scene: &ProductScene, view: &View, roles: &[BodyRole], within: [usize; 4], max: u64) -> Result<u64> {
|
||||
let mut tests = 0;
|
||||
for (role, body) in scene.meshes.iter().enumerate() {
|
||||
let mut looks = BTreeMap::new();
|
||||
for face in &body.faces {
|
||||
validate_face(face)?;
|
||||
// Match the document and task policy: the last assignment wins.
|
||||
looks.insert(face.face, face);
|
||||
}
|
||||
for index in 0..body.mesh.triangles.len() {
|
||||
let look = body.mesh.tri_face.get(index).and_then(|face| looks.get(face)).copied();
|
||||
if let Some(triangle) = view.project(body, index, look, paints(roles, role))? {
|
||||
add_pixel_tests(&mut tests, &triangle, within, max)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(tests)
|
||||
}
|
||||
|
||||
fn reserve<T>(count: usize) -> Result<Vec<T>> {
|
||||
let mut v = Vec::new();
|
||||
v.try_reserve_exact(count).map_err(|_| JobError::new(ErrorCode::AllocationFailed, "could not reserve render work memory"))?;
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// How a picture's pixels are sampled.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Supersample {
|
||||
/// One sample, at the pixel's centre.
|
||||
One,
|
||||
/// 2 × 2 samples, a quarter and three quarters of the way across the pixel, averaged in
|
||||
/// linear light (the product image's).
|
||||
#[default]
|
||||
Two,
|
||||
}
|
||||
|
||||
impl Supersample {
|
||||
fn factor(self) -> usize {
|
||||
match self {
|
||||
Self::One => 1,
|
||||
Self::Two => 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What a body is in a picture.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum BodyRole {
|
||||
/// Drawn in its own colours.
|
||||
#[default]
|
||||
Painted,
|
||||
/// Hides what lies behind it and draws nothing itself (a reference part that hides the model
|
||||
/// while only its lines are drawn).
|
||||
DepthOnly,
|
||||
}
|
||||
|
||||
/// An orthographic view frame: a point `p` lies on paper at `basis.to_local(p) · scale + shift`
|
||||
/// (millimetres, paper y up), seen from the side `basis`'s normal (x × y) points to.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct OrthoFrame {
|
||||
pub basis: Plane,
|
||||
/// Paper millimetres per model millimetre.
|
||||
pub scale: f64,
|
||||
/// Where the basis's own origin lies on paper.
|
||||
pub shift: Vec2,
|
||||
}
|
||||
|
||||
impl OrthoFrame {
|
||||
/// Where `p` lies on paper.
|
||||
pub fn to_paper(&self, p: Vec3) -> Vec2 {
|
||||
self.basis.to_local(p) * self.scale + self.shift
|
||||
}
|
||||
}
|
||||
|
||||
/// What a picture shows and how many pixels it has.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Projection {
|
||||
/// The product camera's view, `width × height` pixels: what [`ImageTask`] draws.
|
||||
Camera { camera: Camera, width: u32, height: u32 },
|
||||
/// An orthographic view frame, on the pixel grid of a paper rectangle.
|
||||
Ortho { frame: OrthoFrame, grid: PixelGrid },
|
||||
}
|
||||
|
||||
impl Projection {
|
||||
fn size(&self) -> (u32, u32) {
|
||||
match self {
|
||||
Self::Camera { width, height, .. } => (*width, *height),
|
||||
Self::Ortho { grid, .. } => (grid.width, grid.height),
|
||||
}
|
||||
}
|
||||
|
||||
/// The clip-space map onto a canvas of `ss` samples per pixel each way, and the eye.
|
||||
fn view(&self, scene: &ProductScene, ss: usize) -> Result<View> {
|
||||
let (width, height) = self.size();
|
||||
if width == 0 || height == 0 || width > MAX_SIDE_PX || height > MAX_SIDE_PX {
|
||||
return Err(limit(&format!("a picture is 1 to {MAX_SIDE_PX} px on a side")));
|
||||
}
|
||||
let (sw, sh) = (width as usize * ss, height as usize * ss);
|
||||
match *self {
|
||||
Self::Camera { camera, .. } => {
|
||||
if !camera.is_valid() {
|
||||
return Err(JobError::new(ErrorCode::InvalidRequest, "invalid image camera"));
|
||||
}
|
||||
let radius = scene.bounds.diagonal().max(10.0) + scene.bounds.center().dist(camera.target);
|
||||
if !radius.is_finite() {
|
||||
return Err(invalid("invalid product bounds"));
|
||||
}
|
||||
Ok(View {
|
||||
vp: camera.view_proj(sw as f64 / sh as f64, radius),
|
||||
back: camera.back(),
|
||||
eye: (camera.fov > 0.0).then(|| camera.eye()),
|
||||
reference: camera.target,
|
||||
sw,
|
||||
sh,
|
||||
})
|
||||
}
|
||||
Self::Ortho { frame, grid } => {
|
||||
let request = |message: &str| JobError::new(ErrorCode::InvalidRequest, message);
|
||||
solidcraft_geom::section::validate_plane(&frame.basis).map_err(|_| request("an orthographic frame needs an orthonormal basis"))?;
|
||||
if !frame.scale.is_finite() || frame.scale <= 0.0 || !frame.shift.is_finite() {
|
||||
return Err(request("an orthographic frame needs a finite scale above 0 and a finite shift"));
|
||||
}
|
||||
if ![grid.x0, grid.y0, grid.pitch].iter().all(|v| v.is_finite()) || grid.pitch <= 0.0 {
|
||||
return Err(request("a picture's pixel grid needs a finite position and pixel size"));
|
||||
}
|
||||
// Depth runs from the nearest corner of the design's box (0) to the farthest (1).
|
||||
let (o, n) = (frame.basis.origin, frame.basis.normal());
|
||||
let b = &scene.bounds;
|
||||
let (mut near, mut far) = (f64::INFINITY, f64::NEG_INFINITY);
|
||||
for corner in 0..8 {
|
||||
let pick = |bit: usize, lo: f64, hi: f64| if corner & bit == 0 { lo } else { hi };
|
||||
let p = Vec3::new(pick(1, b.min.x, b.max.x), pick(2, b.min.y, b.max.y), pick(4, b.min.z, b.max.z));
|
||||
let depth = -(p - o).dot(n);
|
||||
(near, far) = (near.min(depth), far.max(depth));
|
||||
}
|
||||
let range = if far - near > 1e-9 { far - near } else { 1.0 };
|
||||
let near = if near.is_finite() { near } else { 0.0 };
|
||||
if !range.is_finite() {
|
||||
return Err(invalid("invalid product bounds"));
|
||||
}
|
||||
// Paper x and y onto -1..1 across the grid (y up, as clip space), depth onto 0..1.
|
||||
let (bx, by) = (frame.basis.x, frame.basis.y);
|
||||
let (kx, ky) = (2.0 * frame.scale / (f64::from(width) * grid.pitch), 2.0 * frame.scale / (f64::from(height) * grid.pitch));
|
||||
let cx = kx * -o.dot(bx) + 2.0 * (frame.shift.x - grid.x0) / (f64::from(width) * grid.pitch) - 1.0;
|
||||
let cy = ky * -o.dot(by) + 2.0 * (frame.shift.y - grid.y0) / (f64::from(height) * grid.pitch) - 1.0;
|
||||
let (bx, by, nv) = ([bx.x, bx.y, bx.z], [by.x, by.y, by.z], [n.x, n.y, n.z]);
|
||||
let column = |i: usize| [kx * bx[i], ky * by[i], -nv[i] / range, 0.0];
|
||||
let vp = Mat4([column(0), column(1), column(2), [cx, cy, (o.dot(n) - near) / range, 1.0]]);
|
||||
if vp.0.iter().flatten().any(|v| !v.is_finite()) {
|
||||
return Err(request("an orthographic frame too large for its pixel grid"));
|
||||
}
|
||||
Ok(View { vp, back: n, eye: None, reference: o, sw, sh })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A window of a picture's pixels: columns `x0..x1` and rows `y0..y1`, row 0 at the top.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PixelWindow {
|
||||
pub x0: u32,
|
||||
pub y0: u32,
|
||||
pub x1: u32,
|
||||
pub y1: u32,
|
||||
}
|
||||
|
||||
impl PixelWindow {
|
||||
fn width(&self) -> usize {
|
||||
self.x1.saturating_sub(self.x0) as usize
|
||||
}
|
||||
fn height(&self) -> usize {
|
||||
self.y1.saturating_sub(self.y0) as usize
|
||||
}
|
||||
}
|
||||
|
||||
/// At most this many tiles in one picture.
|
||||
pub const MAX_TILES: usize = 65_536;
|
||||
|
||||
/// A picture of a product scene, drawn tile by tile.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PictureRequest {
|
||||
pub projection: Projection,
|
||||
/// The windows drawn, in order: each is handed back as its rows ([`PicturePoll::Tile`]).
|
||||
pub tiles: Vec<PixelWindow>,
|
||||
pub supersample: Supersample,
|
||||
/// One role per body of the scene, in order; empty paints every body.
|
||||
pub roles: Vec<BodyRole>,
|
||||
/// `None`: a transparent background.
|
||||
pub background: Option<[u8; 3]>,
|
||||
}
|
||||
|
||||
/// The checks a picture passes before anything is allocated, and what it costs: the prepared
|
||||
/// lists as a product image's, the pixel scratch of its largest tile.
|
||||
pub fn admit_picture(scene: &ProductScene, request: &PictureRequest) -> Result<Admission> {
|
||||
let (width, height) = request.projection.size();
|
||||
if request.tiles.is_empty() || request.tiles.len() > MAX_TILES {
|
||||
return Err(JobError::new(ErrorCode::InvalidRequest, "a picture is drawn in 1 to 65536 tiles"));
|
||||
}
|
||||
if request.tiles.iter().any(|t| t.x0 >= t.x1 || t.y0 >= t.y1 || t.x1 > width || t.y1 > height) {
|
||||
return Err(JobError::new(ErrorCode::InvalidRequest, "a tile is an empty window or reaches outside its picture"));
|
||||
}
|
||||
if !request.roles.is_empty() && request.roles.len() != scene.meshes.len() {
|
||||
return Err(JobError::new(ErrorCode::InvalidRequest, "a picture gives every body a role, or none"));
|
||||
}
|
||||
let widest = request.tiles.iter().map(|t| t.x1 - t.x0).max().unwrap_or(1);
|
||||
let tallest = request.tiles.iter().map(|t| t.y1 - t.y0).max().unwrap_or(1);
|
||||
let budget = super::CpuBudget::Compact;
|
||||
let limits = budget.limits();
|
||||
let mut admission = admit_raster(admit_source_with_limits(
|
||||
scene,
|
||||
(widest, tallest),
|
||||
MAX_TRIANGLES,
|
||||
limits.max_bodies,
|
||||
limits.max_geometry_bytes,
|
||||
budget.image_limits(false),
|
||||
)?)?;
|
||||
let view = request.projection.view(scene, request.supersample.factor())?;
|
||||
let within = tile_samples(&request.tiles, request.supersample.factor());
|
||||
admission.projected_pixel_tests = Some(projected_pixel_tests(scene, &view, &request.roles, within, MAX_PICTURE_PIXEL_TESTS)?);
|
||||
Ok(admission)
|
||||
}
|
||||
|
||||
/// The map onto the canvas, and where the eye is.
|
||||
struct View {
|
||||
vp: Mat4,
|
||||
/// Toward the eye (an orthographic view's direction).
|
||||
back: Vec3,
|
||||
target: Vec3,
|
||||
/// A perspective view's eye.
|
||||
eye: Option<Vec3>,
|
||||
/// A point the glass's back-to-front depth is measured from.
|
||||
reference: Vec3,
|
||||
/// The canvas's samples each way.
|
||||
sw: usize,
|
||||
sh: usize,
|
||||
}
|
||||
|
||||
impl Projection {
|
||||
fn new(scene: &ProductScene, request: &ImageRequest) -> Result<Self> {
|
||||
let (sw, sh) = (request.width as usize * SS, request.height as usize * SS);
|
||||
let radius = scene.bounds.diagonal().max(10.0) + scene.bounds.center().dist(request.camera.target);
|
||||
if !radius.is_finite() {
|
||||
return Err(invalid("invalid product bounds"));
|
||||
}
|
||||
Ok(Self { vp: request.camera.view_proj(sw as f64 / sh as f64, radius), back: request.camera.back(), target: request.camera.target, sw, sh })
|
||||
}
|
||||
|
||||
fn project(&self, body: &crate::ProductMesh, triangle: usize, look: Option<&FaceAppearance>) -> Result<Option<Triangle>> {
|
||||
/// The same clipping, supersampling and bounds govern admission and raster work.
|
||||
impl View {
|
||||
fn project(&self, body: &crate::ProductMesh, triangle: usize, look: Option<&FaceAppearance>, paint: bool) -> Result<Option<Triangle>> {
|
||||
let mesh = &body.mesh;
|
||||
let indices = mesh.triangles.get(triangle).ok_or_else(|| invalid("missing product triangle"))?;
|
||||
let vertices = indices.map(|i| mesh.positions.get(i as usize).copied());
|
||||
@@ -306,9 +583,14 @@ impl Projection {
|
||||
}
|
||||
// Transmission is physical optical data, never a request to change CPU preview.
|
||||
// Keep the existing scalar sorted-alpha shading for glass previews.
|
||||
let (colour, opacity, surface) = look.map_or((body.color, body.opacity, body.surface.filter(|_| body.transmission.is_none())), |look| {
|
||||
(look.color, look.opacity, look.surface.filter(|_| look.transmission.is_none()))
|
||||
});
|
||||
let (colour, opacity, surface) = if !paint {
|
||||
// A depth-only body is solid, whatever it is made of, and is never shaded.
|
||||
(body.color, 1.0, None)
|
||||
} else {
|
||||
look.map_or((body.color, body.opacity, body.surface.filter(|_| body.transmission.is_none())), |look| {
|
||||
(look.color, look.opacity, look.surface.filter(|_| look.transmission.is_none()))
|
||||
})
|
||||
};
|
||||
if surface.is_some() && opacity != 1.0 {
|
||||
return Err(invalid("opaque PBR requires opacity 1"));
|
||||
}
|
||||
@@ -345,7 +627,7 @@ impl Projection {
|
||||
if !area.is_finite() {
|
||||
return Err(invalid("invalid projected area"));
|
||||
}
|
||||
let centre_depth = (world[0] / 3.0 + world[1] / 3.0 + world[2] / 3.0 - self.target).dot(self.back);
|
||||
let centre_depth = (world[0] / 3.0 + world[1] / 3.0 + world[2] / 3.0 - self.reference).dot(self.back);
|
||||
if !centre_depth.is_finite() {
|
||||
return Err(invalid("invalid triangle depth"));
|
||||
}
|
||||
@@ -357,6 +639,7 @@ impl Projection {
|
||||
colour: colour.map(decode),
|
||||
opacity,
|
||||
surface,
|
||||
paint,
|
||||
centre_depth,
|
||||
area,
|
||||
bounds: [x0 as usize, x1 as usize, y0 as usize, y1 as usize],
|
||||
@@ -366,47 +649,9 @@ impl Projection {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_pixel_tests(sum: &mut u64, triangle: &Triangle) -> Result<()> {
|
||||
let [x0, x1, y0, y1] = triangle.bounds;
|
||||
let tests = ((x1 - x0 + 1) as u64).checked_mul((y1 - y0 + 1) as u64).ok_or_else(|| limit("pixel work overflow"))?;
|
||||
*sum = sum.checked_add(tests).ok_or_else(|| limit("pixel work overflow"))?;
|
||||
if *sum > MAX_PIXEL_TESTS {
|
||||
return Err(limit("projected product exceeds 128000000 pixel tests; reduce image dimensions or visible geometry"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_face(face: &FaceAppearance) -> Result<()> {
|
||||
if !face.opacity.is_finite() || !(0.0..=1.0).contains(&face.opacity) {
|
||||
return Err(invalid("invalid face opacity"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn projected_pixel_tests(scene: &ProductScene, request: &ImageRequest) -> Result<u64> {
|
||||
let projection = Projection::new(scene, request)?;
|
||||
let mut tests = 0;
|
||||
for body in &scene.meshes {
|
||||
let mut looks = BTreeMap::new();
|
||||
for face in &body.faces {
|
||||
validate_face(face)?;
|
||||
// Match the document and task policy: the last assignment wins.
|
||||
looks.insert(face.face, face);
|
||||
}
|
||||
for index in 0..body.mesh.triangles.len() {
|
||||
let look = body.mesh.tri_face.get(index).and_then(|face| looks.get(face)).copied();
|
||||
if let Some(triangle) = projection.project(body, index, look)? {
|
||||
add_pixel_tests(&mut tests, &triangle)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(tests)
|
||||
}
|
||||
|
||||
fn reserve<T>(count: usize) -> Result<Vec<T>> {
|
||||
let mut v = Vec::new();
|
||||
v.try_reserve_exact(count).map_err(|_| JobError::new(ErrorCode::AllocationFailed, "could not reserve render work memory"))?;
|
||||
Ok(v)
|
||||
/// Whether the body at `index` is painted: roles are one per body, or empty for all painted.
|
||||
fn paints(roles: &[BodyRole], index: usize) -> bool {
|
||||
roles.get(index).is_none_or(|role| *role == BodyRole::Painted)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
@@ -430,14 +675,17 @@ struct Triangle {
|
||||
colour: [f64; 3],
|
||||
opacity: f64,
|
||||
surface: Option<crate::surface::OpaqueSurface>,
|
||||
/// Off for a depth-only body.
|
||||
paint: bool,
|
||||
centre_depth: f64,
|
||||
area: f64,
|
||||
/// Whole-canvas sample bounds `[x0, x1, y0, y1]` (inclusive), narrowed to the tile drawn.
|
||||
bounds: [usize; 4],
|
||||
x: usize,
|
||||
y: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Stage {
|
||||
Looks,
|
||||
Prepare,
|
||||
@@ -455,15 +703,32 @@ pub enum TaskPoll {
|
||||
Complete(Vec<u8>),
|
||||
}
|
||||
|
||||
/// CPU product renderer with cooperative preparation, sorting, raster and PNG encoding.
|
||||
/// A step of a [`PictureTask`].
|
||||
pub enum PicturePoll {
|
||||
Pending(Progress),
|
||||
/// One tile, in the order asked: its straight-alpha sRGB RGBA rows, top row first.
|
||||
Tile(PixelWindow, Vec<u8>),
|
||||
/// Every tile has been handed back.
|
||||
Complete,
|
||||
}
|
||||
|
||||
/// The CPU rasteriser both the product image and pictures run: it prepares the scene once
|
||||
/// (appearances, projected triangles, glass sorted back to front), then draws each tile on a
|
||||
/// canvas of its own, sampling at the same whole-picture positions whichever tile a sample falls
|
||||
/// in, so tiles put side by side are the picture drawn whole.
|
||||
/// Transparency sorts triangle centres and blends in linear space; it is not refraction.
|
||||
pub struct ImageTask {
|
||||
struct Raster {
|
||||
scene: ProductScene,
|
||||
request: ImageRequest,
|
||||
admission: Admission,
|
||||
cancel: CancelToken,
|
||||
stage: Stage,
|
||||
projection: Projection,
|
||||
view: View,
|
||||
ss: usize,
|
||||
roles: Vec<BodyRole>,
|
||||
background: Option<[u8; 3]>,
|
||||
tiles: Vec<PixelWindow>,
|
||||
tile: usize,
|
||||
studio_weights: Option<[f64; 3]>,
|
||||
looks: Vec<BTreeMap<u32, FaceAppearance>>,
|
||||
body: usize,
|
||||
item: usize,
|
||||
@@ -481,40 +746,60 @@ pub struct ImageTask {
|
||||
pixels: Vec<[u8; 4]>,
|
||||
depth: Vec<f32>,
|
||||
pixel_tests: u64,
|
||||
max_pixel_tests: u64,
|
||||
/// The sample bounds round the tiles ([`tile_samples`]).
|
||||
within: [usize; 4],
|
||||
pixel_done: u64,
|
||||
draw: usize,
|
||||
triangle: Option<Triangle>,
|
||||
output: Vec<u8>,
|
||||
encoder: Option<super::encoding::PngEncoding>,
|
||||
}
|
||||
|
||||
impl ImageTask {
|
||||
pub fn new(scene: ProductScene, request: ImageRequest, cancel: CancelToken) -> Result<Self> {
|
||||
cancel.check()?;
|
||||
if request.integrator != super::Integrator::Raster {
|
||||
return Err(JobError::new(ErrorCode::InvalidRequest, "raster task cannot execute a raytrace request"));
|
||||
}
|
||||
let admission = admit(&scene, &request)?;
|
||||
let projection = Projection::new(&scene, &request)?;
|
||||
let (sw, sh) = (projection.sw, projection.sh);
|
||||
enum RasterPoll {
|
||||
Pending,
|
||||
Tile(PixelWindow, Vec<u8>),
|
||||
}
|
||||
|
||||
impl Raster {
|
||||
fn new(
|
||||
scene: ProductScene,
|
||||
request: PictureRequest,
|
||||
studio_weights: Option<[f64; 3]>,
|
||||
admission: Admission,
|
||||
max_pixel_tests: u64,
|
||||
cancel: CancelToken,
|
||||
) -> Result<Self> {
|
||||
let PictureRequest { projection, tiles, supersample, roles, background } = request;
|
||||
let ss = supersample.factor();
|
||||
let view = projection.view(&scene, ss)?;
|
||||
let within = tile_samples(&tiles, ss);
|
||||
let samples = tiles.iter().map(|t| t.width() * t.height()).max().unwrap_or(0) * ss * ss;
|
||||
let pixels = tiles.iter().map(|t| t.width() * t.height()).max().unwrap_or(0);
|
||||
let looks = reserve(scene.meshes.len())?;
|
||||
let (opaque, glass, scratch) =
|
||||
(reserve(admission.triangles as usize)?, reserve(admission.triangles as usize)?, reserve(admission.triangles as usize)?);
|
||||
let (pixels, depth, output) = (reserve(sw * sh)?, reserve(sw * sh)?, reserve(request.width as usize * request.height as usize * 4)?);
|
||||
Ok(Self {
|
||||
scene,
|
||||
request,
|
||||
admission,
|
||||
cancel,
|
||||
stage: Stage::Looks,
|
||||
projection,
|
||||
looks,
|
||||
body: 0,
|
||||
item: 0,
|
||||
prepare_done: 0,
|
||||
opaque,
|
||||
glass,
|
||||
scratch,
|
||||
pixels: reserve(samples)?,
|
||||
depth: reserve(samples)?,
|
||||
output: reserve(pixels * 4)?,
|
||||
scene,
|
||||
admission,
|
||||
cancel,
|
||||
stage: Stage::Looks,
|
||||
view,
|
||||
ss,
|
||||
roles,
|
||||
background,
|
||||
tiles,
|
||||
tile: 0,
|
||||
studio_weights,
|
||||
body: 0,
|
||||
item: 0,
|
||||
prepare_done: 0,
|
||||
sort_width: 1,
|
||||
sort_block: 0,
|
||||
sort_left: 0,
|
||||
@@ -522,65 +807,62 @@ impl ImageTask {
|
||||
sort_mid: 0,
|
||||
sort_end: 0,
|
||||
sort_dst: 0,
|
||||
pixels,
|
||||
depth,
|
||||
pixel_tests: 0,
|
||||
max_pixel_tests,
|
||||
within,
|
||||
pixel_done: 0,
|
||||
draw: 0,
|
||||
triangle: None,
|
||||
output,
|
||||
encoder: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn progress(&self) -> Progress {
|
||||
/// A sample nothing has been drawn on.
|
||||
fn clear(&self) -> [u8; 4] {
|
||||
self.background.map_or([0; 4], |[r, g, b]| [r, g, b, 255])
|
||||
}
|
||||
|
||||
fn window(&self) -> Result<PixelWindow> {
|
||||
self.tiles.get(self.tile).copied().ok_or_else(|| invalid("missing picture tile"))
|
||||
}
|
||||
|
||||
fn progress(&self) -> Progress {
|
||||
match self.stage {
|
||||
Stage::Looks | Stage::Prepare | Stage::Sort => Progress {
|
||||
phase: Phase::Preparing,
|
||||
done: self.prepare_done,
|
||||
total: self.admission.face_appearances + self.admission.triangles + self.scene.meshes.len() as u64 + self.glass.len() as u64 * 19,
|
||||
},
|
||||
Stage::Clear | Stage::Opaque | Stage::Glass | Stage::Downsample => Progress {
|
||||
phase: Phase::Running,
|
||||
done: self.pixel_done,
|
||||
total: self.pixel_tests
|
||||
+ (self.projection.sw * self.projection.sh) as u64
|
||||
+ u64::from(self.request.width) * u64::from(self.request.height),
|
||||
},
|
||||
Stage::Encode => Progress {
|
||||
phase: Phase::Encoding,
|
||||
done: self.encoder.as_ref().map_or(0, |encoder| encoder.row() as u64),
|
||||
total: u64::from(self.request.height),
|
||||
},
|
||||
Stage::Clear | Stage::Opaque | Stage::Glass | Stage::Downsample | Stage::Encode => {
|
||||
// In u64: a large picture's canvas outgrows a 32-bit usize.
|
||||
let [x0, x1, y0, y1] = self.within;
|
||||
let samples = (x1 - x0 + 1) as u64 * (y1 - y0 + 1) as u64;
|
||||
let total = self.pixel_tests + samples + samples / (self.ss * self.ss) as u64;
|
||||
// Overlapping tiles can draw more than the picture once.
|
||||
Progress { phase: Phase::Running, done: self.pixel_done.min(total), total }
|
||||
}
|
||||
Stage::Done => Progress { phase: Phase::Completed, done: 1, total: 1 },
|
||||
}
|
||||
}
|
||||
|
||||
/// At most 16384 simple units; encoding additionally yields after at most four rows.
|
||||
pub fn step(&mut self, units: usize) -> Result<TaskPoll> {
|
||||
let mut rows = 0;
|
||||
/// At most `units` simple units, or up to the end of a tile.
|
||||
fn step(&mut self, units: usize) -> Result<RasterPoll> {
|
||||
for _ in 0..units.clamp(1, MAX_STEP) {
|
||||
self.cancel.check()?;
|
||||
match self.stage {
|
||||
Stage::Looks => self.look_step()?,
|
||||
Stage::Prepare => self.prepare_step()?,
|
||||
Stage::Sort => self.sort_step()?,
|
||||
Stage::Clear => self.clear_step(),
|
||||
Stage::Clear => self.clear_step()?,
|
||||
Stage::Opaque | Stage::Glass => self.draw_step()?,
|
||||
Stage::Downsample => self.downsample_step()?,
|
||||
Stage::Encode => {
|
||||
if let Some(png) = self.encode_step()? {
|
||||
return Ok(TaskPoll::Complete(png));
|
||||
}
|
||||
rows += 1;
|
||||
if rows >= 4 {
|
||||
break;
|
||||
Stage::Downsample => {
|
||||
if let Some(tile) = self.downsample_step()? {
|
||||
return Ok(tile);
|
||||
}
|
||||
}
|
||||
Stage::Done => return Err(JobError::new(ErrorCode::NotCompleted, "render result already consumed")),
|
||||
Stage::Encode | Stage::Done => return Err(JobError::new(ErrorCode::NotCompleted, "render result already consumed")),
|
||||
}
|
||||
}
|
||||
Ok(TaskPoll::Pending(self.progress()))
|
||||
Ok(RasterPoll::Pending)
|
||||
}
|
||||
|
||||
fn look_step(&mut self) -> Result<()> {
|
||||
@@ -611,7 +893,7 @@ impl ImageTask {
|
||||
let body = self.scene.meshes.get(draw.body as usize).ok_or_else(|| invalid("missing product body"))?;
|
||||
let look =
|
||||
self.looks.get(draw.body as usize).and_then(|looks| body.mesh.tri_face.get(draw.triangle as usize).and_then(|face| looks.get(face)));
|
||||
self.projection.project(body, draw.triangle as usize, look)
|
||||
self.view.project(body, draw.triangle as usize, look, paints(&self.roles, draw.body as usize))
|
||||
}
|
||||
|
||||
fn prepare_step(&mut self) -> Result<()> {
|
||||
@@ -628,7 +910,7 @@ impl ImageTask {
|
||||
}
|
||||
let mut draw = Draw { body: self.body as u32, triangle: self.item as u32, depth: 0.0 };
|
||||
if let Some(triangle) = self.project(draw)? {
|
||||
add_pixel_tests(&mut self.pixel_tests, &triangle)?;
|
||||
add_pixel_tests(&mut self.pixel_tests, &triangle, self.within, self.max_pixel_tests)?;
|
||||
draw.depth = triangle.centre_depth;
|
||||
if triangle.opacity >= 1.0 {
|
||||
self.opaque.push(draw);
|
||||
@@ -688,19 +970,24 @@ impl ImageTask {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_step(&mut self) {
|
||||
if self.pixels.len() >= self.projection.sw * self.projection.sh {
|
||||
fn clear_step(&mut self) -> Result<()> {
|
||||
let window = self.window()?;
|
||||
if self.pixels.len() >= window.width() * window.height() * self.ss * self.ss {
|
||||
self.stage = Stage::Opaque;
|
||||
self.draw = 0;
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
let [r, g, b] = self.request.background;
|
||||
self.pixels.push(if self.request.transparent_background { [0; 4] } else { [r, g, b, 255] });
|
||||
self.pixels.push(self.clear());
|
||||
self.depth.push(f32::INFINITY);
|
||||
self.pixel_done += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_step(&mut self) -> Result<()> {
|
||||
let window = self.window()?;
|
||||
// The tile's canvas samples, in whole-canvas coordinates.
|
||||
let (cx0, cx1) = (window.x0 as usize * self.ss, window.x1 as usize * self.ss - 1);
|
||||
let (cy0, cy1) = (window.y0 as usize * self.ss, window.y1 as usize * self.ss - 1);
|
||||
if self.triangle.is_none() {
|
||||
let list = if self.stage == Stage::Opaque { &self.opaque } else { &self.glass };
|
||||
let Some(draw) = list.get(self.draw).copied() else {
|
||||
@@ -708,15 +995,22 @@ impl ImageTask {
|
||||
self.draw = 0;
|
||||
return Ok(());
|
||||
};
|
||||
self.triangle = self.project(draw)?;
|
||||
// Only the part of the triangle's bounds inside this tile.
|
||||
self.triangle = self.project(draw)?.and_then(|mut t| {
|
||||
narrowed(t.bounds, [cx0, cx1, cy0, cy1]).map(|bounds| {
|
||||
(t.bounds, t.x, t.y) = (bounds, bounds[0], bounds[2]);
|
||||
t
|
||||
})
|
||||
});
|
||||
if self.triangle.is_none() {
|
||||
self.draw += 1;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let clear = self.clear();
|
||||
let triangle = self.triangle.as_mut().ok_or_else(|| invalid("missing raster triangle"))?;
|
||||
let eye = (self.request.camera.fov > 0.0).then(|| self.request.camera.eye());
|
||||
raster_pixel(triangle, self.projection.sw, self.projection.back, eye, self.request.studio_weights, &mut self.pixels, &mut self.depth)?;
|
||||
let tile = (cx0, cy0, cx1 - cx0 + 1);
|
||||
raster_pixel(triangle, tile, self.view.back, self.view.eye, self.studio_weights, clear, &mut self.pixels, &mut self.depth)?;
|
||||
triangle.x += 1;
|
||||
if triangle.x > triangle.bounds[1] {
|
||||
triangle.x = triangle.bounds[0];
|
||||
@@ -730,28 +1024,37 @@ impl ImageTask {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn downsample_step(&mut self) -> Result<()> {
|
||||
let width = self.request.width as usize;
|
||||
/// One output pixel of the tile; the tile's rows once its last pixel is in.
|
||||
fn downsample_step(&mut self) -> Result<Option<RasterPoll>> {
|
||||
let window = self.window()?;
|
||||
let (width, ss) = (window.width(), self.ss);
|
||||
let index = self.output.len() / 4;
|
||||
if index >= width * self.request.height as usize {
|
||||
self.encoder =
|
||||
Some(super::encoding::PngEncoding::new(self.request.width, self.request.height, self.admission.encoded_limit, self.cancel.clone())?);
|
||||
self.stage = Stage::Encode;
|
||||
// Pixels and prepared geometry are no longer needed during PNG compression.
|
||||
self.pixels = Vec::new();
|
||||
self.depth = Vec::new();
|
||||
self.opaque = Vec::new();
|
||||
self.glass = Vec::new();
|
||||
self.scratch = Vec::new();
|
||||
self.looks = Vec::new();
|
||||
self.scene.meshes.clear();
|
||||
return Ok(());
|
||||
if index >= width * window.height() {
|
||||
let rows = std::mem::take(&mut self.output);
|
||||
self.tile += 1;
|
||||
if self.tile < self.tiles.len() {
|
||||
self.output = reserve(rows.capacity())?;
|
||||
self.pixels.clear();
|
||||
self.depth.clear();
|
||||
self.stage = Stage::Clear;
|
||||
} else {
|
||||
// Pixels and prepared geometry are no longer needed.
|
||||
self.pixels = Vec::new();
|
||||
self.depth = Vec::new();
|
||||
self.opaque = Vec::new();
|
||||
self.glass = Vec::new();
|
||||
self.scratch = Vec::new();
|
||||
self.looks = Vec::new();
|
||||
self.scene.meshes.clear();
|
||||
self.stage = Stage::Done;
|
||||
}
|
||||
return Ok(Some(RasterPoll::Tile(window, rows)));
|
||||
}
|
||||
let (x, y) = (index % width, index / width);
|
||||
let (mut colour, mut alpha) = ([0.0; 3], 0.0);
|
||||
for dy in 0..SS {
|
||||
for dx in 0..SS {
|
||||
let p = self.pixels.get((y * SS + dy) * self.projection.sw + x * SS + dx).ok_or_else(|| invalid("missing downsample pixel"))?;
|
||||
for dy in 0..ss {
|
||||
for dx in 0..ss {
|
||||
let p = self.pixels.get((y * ss + dy) * width * ss + x * ss + dx).ok_or_else(|| invalid("missing downsample pixel"))?;
|
||||
let a = f64::from(p[3]) / 255.0;
|
||||
for (sum, channel) in colour.iter_mut().zip(p.iter().take(3)) {
|
||||
*sum += decode(*channel) * a;
|
||||
@@ -760,32 +1063,127 @@ impl ImageTask {
|
||||
}
|
||||
}
|
||||
let rgb = colour.map(|v| if alpha > 1e-12 { encode(v / alpha) } else { 0 });
|
||||
self.output.extend_from_slice(&[rgb[0], rgb[1], rgb[2], (alpha * 255.0 / (SS * SS) as f64).round() as u8]);
|
||||
self.output.extend_from_slice(&[rgb[0], rgb[1], rgb[2], (alpha * 255.0 / (ss * ss) as f64).round() as u8]);
|
||||
self.pixel_done += 1;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encode_step(&mut self) -> Result<Option<Vec<u8>>> {
|
||||
let result = self.encoder.as_mut().ok_or_else(|| invalid("missing PNG writer"))?.step(&self.output)?;
|
||||
if result.is_some() {
|
||||
self.output = Vec::new();
|
||||
self.stage = Stage::Done;
|
||||
}
|
||||
Ok(result)
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// A picture drawn tile by tile ([`PictureRequest`]).
|
||||
pub struct PictureTask {
|
||||
raster: Raster,
|
||||
}
|
||||
|
||||
impl PictureTask {
|
||||
pub fn new(scene: ProductScene, request: PictureRequest, cancel: CancelToken) -> Result<Self> {
|
||||
cancel.check()?;
|
||||
let admission = admit_picture(&scene, &request)?;
|
||||
let max_tests = MAX_PICTURE_PIXEL_TESTS;
|
||||
Ok(Self { raster: Raster::new(scene, request, None, admission, max_tests, cancel)? })
|
||||
}
|
||||
|
||||
pub fn progress(&self) -> Progress {
|
||||
self.raster.progress()
|
||||
}
|
||||
|
||||
/// At most 16384 simple units, or up to the end of a tile.
|
||||
pub fn step(&mut self, units: usize) -> Result<PicturePoll> {
|
||||
if self.raster.stage == Stage::Done {
|
||||
return Ok(PicturePoll::Complete);
|
||||
}
|
||||
Ok(match self.raster.step(units)? {
|
||||
RasterPoll::Pending => PicturePoll::Pending(self.raster.progress()),
|
||||
RasterPoll::Tile(window, rows) => PicturePoll::Tile(window, rows),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// CPU product renderer with cooperative preparation, sorting, raster and PNG encoding: a
|
||||
/// [`Raster`] of one whole-picture tile from the product camera, then its PNG.
|
||||
pub struct ImageTask {
|
||||
raster: Raster,
|
||||
width: u32,
|
||||
height: u32,
|
||||
output: Vec<u8>,
|
||||
encoder: Option<super::encoding::PngEncoding>,
|
||||
}
|
||||
|
||||
impl ImageTask {
|
||||
pub fn new(scene: ProductScene, request: ImageRequest, cancel: CancelToken) -> Result<Self> {
|
||||
cancel.check()?;
|
||||
if request.integrator != super::Integrator::Raster {
|
||||
return Err(JobError::new(ErrorCode::InvalidRequest, "raster task cannot execute a raytrace request"));
|
||||
}
|
||||
let admission = admit(&scene, &request)?;
|
||||
let (width, height) = (request.width, request.height);
|
||||
let picture = PictureRequest {
|
||||
projection: Projection::Camera { camera: request.camera, width, height },
|
||||
tiles: vec![PixelWindow { x0: 0, y0: 0, x1: width, y1: height }],
|
||||
supersample: Supersample::Two,
|
||||
roles: Vec::new(),
|
||||
background: (!request.transparent_background).then_some(request.background),
|
||||
};
|
||||
let raster = Raster::new(scene, picture, request.studio_weights, admission, MAX_PIXEL_TESTS, cancel)?;
|
||||
Ok(Self { raster, width, height, output: Vec::new(), encoder: None })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn stage(&self) -> Stage {
|
||||
self.raster.stage
|
||||
}
|
||||
|
||||
pub fn progress(&self) -> Progress {
|
||||
match (&self.encoder, self.raster.stage) {
|
||||
(Some(encoder), Stage::Encode) => Progress { phase: Phase::Encoding, done: encoder.row() as u64, total: u64::from(self.height) },
|
||||
_ => self.raster.progress(),
|
||||
}
|
||||
}
|
||||
|
||||
/// At most 16384 simple units; encoding additionally yields after at most four rows.
|
||||
pub fn step(&mut self, units: usize) -> Result<TaskPoll> {
|
||||
if let Some(encoder) = self.encoder.as_mut() {
|
||||
for _ in 0..units.clamp(1, 4) {
|
||||
self.raster.cancel.check()?;
|
||||
if let Some(png) = encoder.step(&self.output)? {
|
||||
self.output = Vec::new();
|
||||
self.encoder = None;
|
||||
self.raster.stage = Stage::Done;
|
||||
return Ok(TaskPoll::Complete(png));
|
||||
}
|
||||
}
|
||||
return Ok(TaskPoll::Pending(self.progress()));
|
||||
}
|
||||
if self.raster.stage == Stage::Done {
|
||||
return Err(JobError::new(ErrorCode::NotCompleted, "render result already consumed"));
|
||||
}
|
||||
if let RasterPoll::Tile(_, rows) = self.raster.step(units)? {
|
||||
self.output = rows;
|
||||
let limit = self.raster.admission.encoded_limit;
|
||||
self.encoder = Some(super::encoding::PngEncoding::new(self.width, self.height, limit, self.raster.cancel.clone())?);
|
||||
// The tile is the whole picture: the raster has handed it over and the PNG is written.
|
||||
self.raster.stage = Stage::Encode;
|
||||
}
|
||||
Ok(TaskPoll::Pending(self.progress()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Shades and stores one sample: the triangle's at its cursor `(t.x, t.y)`, in a tile whose
|
||||
/// canvas starts at whole-canvas sample `(x0, y0)` and is `width` samples wide. A depth-only
|
||||
/// triangle in front puts back `clear`, so it hides what was drawn behind it in either order.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn raster_pixel(
|
||||
t: &Triangle,
|
||||
width: usize,
|
||||
(x0, y0, width): (usize, usize, usize),
|
||||
back: Vec3,
|
||||
eye: Option<Vec3>,
|
||||
studio_weights: Option<[f64; 3]>,
|
||||
clear: [u8; 4],
|
||||
pixels: &mut [[u8; 4]],
|
||||
depth: &mut [f32],
|
||||
) -> Result<()> {
|
||||
let [a, b, c] = t.points;
|
||||
// The sample's position from its whole-canvas integer index, so it is the same number in
|
||||
// every tile and tiles side by side give the whole picture exactly.
|
||||
let (x, y) = (t.x as f64 + 0.5, t.y as f64 + 0.5);
|
||||
let w0 = ((b[0] - x) * (c[1] - y) - (b[1] - y) * (c[0] - x)) / t.area;
|
||||
let w1 = ((c[0] - x) * (a[1] - y) - (c[1] - y) * (a[0] - x)) / t.area;
|
||||
@@ -806,11 +1204,16 @@ fn raster_pixel(
|
||||
}
|
||||
}
|
||||
let z = (w0 * a[2] + w1 * b[2] + w2 * c[2]) as f32;
|
||||
let index = t.y * width + t.x;
|
||||
let index = (t.y - y0) * width + (t.x - x0);
|
||||
let old_depth = depth.get_mut(index).ok_or_else(|| invalid("missing pixel depth"))?;
|
||||
if z > *old_depth {
|
||||
return Ok(());
|
||||
}
|
||||
if !t.paint {
|
||||
*old_depth = z;
|
||||
*pixels.get_mut(index).ok_or_else(|| invalid("missing colour pixel"))? = clear;
|
||||
return Ok(());
|
||||
}
|
||||
let weights = [w0 * t.inv_w[0], w1 * t.inv_w[1], w2 * t.inv_w[2]];
|
||||
let sum: f64 = weights.iter().sum();
|
||||
if !sum.is_finite() || sum <= 1e-12 {
|
||||
|
||||
@@ -124,7 +124,7 @@ fn cancellation_is_observed_during_preparation_sort_pixels_and_encoding() {
|
||||
let mut task = ImageTask::new(scene(vec![square("Glass", [40, 180, 100], 0.4, Vec3::ZERO)]), request(), cancel.clone()).unwrap();
|
||||
let mut reached = false;
|
||||
for _ in 0..100_000 {
|
||||
if task.stage == stage {
|
||||
if task.stage() == stage {
|
||||
reached = true;
|
||||
break;
|
||||
}
|
||||
@@ -139,7 +139,7 @@ fn cancellation_is_observed_during_preparation_sort_pixels_and_encoding() {
|
||||
#[test]
|
||||
fn encoding_yields_between_rows_and_completed_result_is_consumed_once() {
|
||||
let mut task = ImageTask::new(scene(Vec::new()), request(), CancelToken::default()).unwrap();
|
||||
while task.stage != Stage::Encode {
|
||||
while task.stage() != Stage::Encode {
|
||||
assert!(matches!(task.step(64).unwrap(), TaskPoll::Pending(_)));
|
||||
}
|
||||
let before = task.encoder.as_ref().unwrap().row();
|
||||
@@ -176,6 +176,35 @@ fn invalid_sizes_indices_opacity_and_large_work_lists_fail_without_a_frame() {
|
||||
assert_eq!(std::mem::size_of::<Draw>(), 16, "admission accounts for each of the three lists");
|
||||
}
|
||||
|
||||
/// What an image request must be is checked on each way in, a registered executor's too.
|
||||
#[test]
|
||||
fn every_way_in_checks_what_an_image_request_must_be() {
|
||||
let scene = scene(vec![square("Square", [100; 3], 1.0, Vec3::ZERO)]);
|
||||
let budget = crate::job::CpuBudget::Compact;
|
||||
let limits = budget.limits();
|
||||
let refused = |r: &ImageRequest| {
|
||||
[
|
||||
admit_geometry(&scene, r, MAX_TRIANGLES).err().map(|e| e.code),
|
||||
admit_geometry_with_limits(&scene, r, MAX_TRIANGLES, limits.max_bodies, limits.max_geometry_bytes, budget.image_limits(false))
|
||||
.err()
|
||||
.map(|e| e.code),
|
||||
]
|
||||
};
|
||||
let invalid = [Some(ErrorCode::InvalidRequest), Some(ErrorCode::InvalidRequest)];
|
||||
assert_eq!(refused(&request()), [None, None]);
|
||||
let mut camera = request();
|
||||
camera.camera.distance = f64::NAN;
|
||||
assert_eq!(refused(&camera), invalid, "a camera that isn't valid");
|
||||
for value in [-0.1, 1.1, f64::NAN] {
|
||||
let weights = ImageRequest { studio_weights: Some([value, 0.0, 0.0]), ..request() };
|
||||
assert_eq!(refused(&weights), invalid, "a weight of {value}");
|
||||
}
|
||||
let lit = ImageRequest { studio_weights: Some([1.0; 3]), ..request() };
|
||||
assert_eq!(refused(&lit), [None, None]);
|
||||
let traced = ImageRequest { integrator: crate::job::Integrator::Raytrace, ..lit };
|
||||
assert_eq!(refused(&traced), invalid, "studio weights on a ray traced image");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registered_executor_geometry_admission_excludes_the_cpu_raster_estimate() {
|
||||
// A library-bounded source: about 100 MiB of face appearances on a small mesh at the
|
||||
@@ -230,10 +259,10 @@ fn pixel_work_admission_includes_the_limit_and_uses_clipped_sample_bounds() {
|
||||
let mut scene = scene(vec![body]);
|
||||
assert_eq!(admit(&scene, &request).unwrap().projected_pixel_tests, Some(128_000_000));
|
||||
let mut task = ImageTask::new(scene.clone(), request.clone(), CancelToken::default()).unwrap();
|
||||
while task.stage != Stage::Clear {
|
||||
while task.stage() != Stage::Clear {
|
||||
task.step(1).unwrap();
|
||||
}
|
||||
assert_eq!(task.pixel_tests, 128_000_000);
|
||||
assert_eq!(task.raster.pixel_tests, 128_000_000);
|
||||
Arc::make_mut(&mut scene.meshes[0].mesh).triangles.push([0, 1, 2]);
|
||||
assert_eq!(admit(&scene, &request).unwrap_err().code, ErrorCode::LimitExceeded);
|
||||
// Registered executors budget their own work, independently of the raster projection.
|
||||
@@ -333,3 +362,366 @@ fn product_render_output_is_pinned() {
|
||||
assert_eq!(got, want, "{name}: the rows changed");
|
||||
}
|
||||
}
|
||||
|
||||
/// Every tile of a picture, stepped to the end, stitched into the whole picture's rows.
|
||||
fn picture(scene: ProductScene, request: PictureRequest) -> Vec<u8> {
|
||||
let (width, height) = request.projection.size();
|
||||
let mut task = PictureTask::new(scene, request, CancelToken::default()).unwrap();
|
||||
let mut rows = vec![0; width as usize * height as usize * 4];
|
||||
for _ in 0..1_000_000 {
|
||||
match task.step(4096).unwrap() {
|
||||
PicturePoll::Pending(p) => assert!(p.done <= p.total, "{p:?}"),
|
||||
PicturePoll::Tile(w, tile) => {
|
||||
assert_eq!(tile.len(), (w.x1 - w.x0) as usize * (w.y1 - w.y0) as usize * 4);
|
||||
for (y, row) in (w.y0..w.y1).zip(tile.chunks_exact((w.x1 - w.x0) as usize * 4)) {
|
||||
let at = (y as usize * width as usize + w.x0 as usize) * 4;
|
||||
rows[at..at + row.len()].copy_from_slice(row);
|
||||
}
|
||||
}
|
||||
PicturePoll::Complete => return rows,
|
||||
}
|
||||
}
|
||||
panic!("bounded picture did not finish");
|
||||
}
|
||||
|
||||
/// `columns × rows` tiles of uneven sizes covering a `width × height` picture.
|
||||
fn tiles(width: u32, height: u32, columns: u32, rows: u32) -> Vec<PixelWindow> {
|
||||
let cut = |n: u32, parts: u32, i: u32| if i == parts { n } else { (n * i / parts + i % 2).min(n) };
|
||||
let mut out = Vec::new();
|
||||
for j in 0..rows {
|
||||
for i in 0..columns {
|
||||
out.push(PixelWindow {
|
||||
x0: cut(width, columns, i),
|
||||
y0: cut(height, rows, j),
|
||||
x1: cut(width, columns, i + 1),
|
||||
y1: cut(height, rows, j + 1),
|
||||
});
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A front view: paper x along +X, paper y along +Z, seen from −Y; 1 mm pixels on a
|
||||
/// 100 × 50 mm rectangle with its lower-left corner at the paper origin; the model's origin at
|
||||
/// paper (50, 25).
|
||||
fn front_view() -> (OrthoFrame, PixelGrid) {
|
||||
let frame = OrthoFrame { basis: Plane { origin: Vec3::ZERO, x: Vec3::X, y: Vec3::Z }, scale: 1.0, shift: Vec2::new(50.0, 25.0) };
|
||||
(frame, PixelGrid::new(0.0, 0.0, 100.0, 50.0, 25.4).unwrap())
|
||||
}
|
||||
|
||||
fn opaque_and_glass() -> ProductScene {
|
||||
let mut glass = block("Glass", [40, 200, 120], Vec3::new(6.0, -14.0, 4.0), Vec3::new(9.0, 2.0, 9.0));
|
||||
glass.opacity = 0.4;
|
||||
scene(vec![
|
||||
block("Block", [180, 120, 60], Vec3::ZERO, Vec3::new(20.0, 12.0, 8.0)),
|
||||
block("Post", [60, 140, 200], Vec3::new(0.0, 0.0, 16.0), Vec3::new(4.0, 4.0, 8.0)),
|
||||
glass,
|
||||
])
|
||||
}
|
||||
|
||||
/// The tiles of a picture, put side by side, are the picture drawn as one tile, to the byte:
|
||||
/// in the camera's view and an orthographic frame, at both samplings, with glass and with uneven
|
||||
/// tiles that split triangles anywhere.
|
||||
#[test]
|
||||
fn tiles_side_by_side_are_the_picture_drawn_whole() {
|
||||
let mut iso = Camera { distance: 120.0, ..Default::default() };
|
||||
iso.set_view(StandardView::Iso);
|
||||
let (frame, grid) = front_view();
|
||||
for projection in [Projection::Camera { camera: Camera { fov: 0.6, ..iso }, width: 61, height: 37 }, Projection::Ortho { frame, grid }] {
|
||||
let (width, height) = projection.size();
|
||||
for supersample in [Supersample::One, Supersample::Two] {
|
||||
let request = |tiles: Vec<PixelWindow>| PictureRequest { projection, tiles, supersample, roles: Vec::new(), background: None };
|
||||
let whole = picture(opaque_and_glass(), request(tiles(width, height, 1, 1)));
|
||||
assert!(whole.as_chunks::<4>().0.iter().filter(|p| p[3] > 0).count() > 200, "{projection:?}: the scene is in the picture");
|
||||
for (columns, rows) in [(2, 2), (7, 3), (1, 9)] {
|
||||
let stitched = picture(opaque_and_glass(), request(tiles(width, height, columns, rows)));
|
||||
assert!(stitched == whole, "{projection:?} {supersample:?}: {columns} × {rows} tiles differ from the whole picture");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The product image is the picture of its camera in one tile, 2 × 2 sampled, then encoded.
|
||||
#[test]
|
||||
fn the_product_image_is_its_cameras_picture() {
|
||||
let mut iso = Camera { distance: 120.0, ..Default::default() };
|
||||
iso.set_view(StandardView::Iso);
|
||||
for transparent in [true, false] {
|
||||
let request = ImageRequest { camera: iso, width: 48, height: 40, transparent_background: transparent, ..request() };
|
||||
let png = finish(ImageTask::new(opaque_and_glass(), request.clone(), CancelToken::default()).unwrap());
|
||||
let image = image::load_from_memory(&png).unwrap().to_rgba8().into_raw();
|
||||
let picture = picture(
|
||||
opaque_and_glass(),
|
||||
PictureRequest {
|
||||
projection: Projection::Camera { camera: iso, width: 48, height: 40 },
|
||||
tiles: tiles(48, 40, 3, 2),
|
||||
supersample: Supersample::Two,
|
||||
roles: Vec::new(),
|
||||
background: (!transparent).then_some(request.background),
|
||||
},
|
||||
);
|
||||
assert!(image == picture, "transparent {transparent}");
|
||||
}
|
||||
}
|
||||
|
||||
/// An orthographic picture lands on its paper rectangle's pixel grid: a 20 × 10 mm face whose
|
||||
/// edges lie on pixel edges covers exactly its 20 × 10 pixels sampled once at the centres (the
|
||||
/// grid's rows run top-down from the rectangle's top); edges a little inside a pixel give it the
|
||||
/// share of its 2 × 2 samples they cover, in the face's own colour (straight alpha).
|
||||
#[test]
|
||||
fn an_orthographic_picture_lands_on_its_pixel_grid() {
|
||||
let (frame, grid) = front_view();
|
||||
// A box seen from the front: paper x from `left` to 60, y from 20 to `top`.
|
||||
let face = |left: f64, top: f64| {
|
||||
let (x0, z1) = (left - 50.0, top - 25.0);
|
||||
let half = Vec3::new((10.0 - x0) / 2.0, 3.0, (z1 + 5.0) / 2.0);
|
||||
scene(vec![block("Face", [200, 100, 50], Vec3::new((x0 + 10.0) / 2.0, 0.0, (z1 - 5.0) / 2.0), half)])
|
||||
};
|
||||
let request = |supersample| PictureRequest {
|
||||
projection: Projection::Ortho { frame, grid },
|
||||
tiles: tiles(100, 50, 1, 1),
|
||||
supersample,
|
||||
roles: Vec::new(),
|
||||
background: None,
|
||||
};
|
||||
let px = |rows: &[u8], x: usize, y: usize| <[u8; 4]>::try_from(&rows[(y * 100 + x) * 4..(y * 100 + x) * 4 + 4]).unwrap();
|
||||
// Paper x 40..60, y 20..30: columns 40..60 and, counted from the top of the 50 px grid, rows 20..30.
|
||||
let rows = picture(face(40.0, 30.0), request(Supersample::One));
|
||||
for y in 0..50 {
|
||||
for x in 0..100 {
|
||||
let inside = (40..60).contains(&x) && (20..30).contains(&y);
|
||||
assert_eq!(px(&rows, x, y)[3], if inside { 255 } else { 0 }, "pixel ({x}, {y})");
|
||||
}
|
||||
}
|
||||
// The left edge at paper x 39.6 and the top at 30.4, sampled once: column 39's centre (39.5)
|
||||
// and row 19's (30.5) lie outside, so neither is drawn.
|
||||
let rows = picture(face(39.6, 30.4), request(Supersample::One));
|
||||
assert_eq!((px(&rows, 39, 25)[3], px(&rows, 50, 19)[3], px(&rows, 40, 20)[3]), (0, 0, 255));
|
||||
// Sampled 2 × 2: column 39 is half covered, row 19 half, their corner a quarter; each in the
|
||||
// face's colour.
|
||||
let rows = picture(face(39.6, 30.4), request(Supersample::Two));
|
||||
let inner = px(&rows, 50, 25);
|
||||
assert_eq!(inner[3], 255);
|
||||
for ((x, y), alpha) in [((39, 25), 128), ((50, 19), 128), ((39, 19), 64), ((38, 25), 0), ((50, 18), 0)] {
|
||||
let p = px(&rows, x, y);
|
||||
assert_eq!(p[3], alpha, "pixel ({x}, {y})");
|
||||
if alpha > 0 {
|
||||
assert_eq!(p[..3], inner[..3], "pixel ({x}, {y}) keeps the face's colour");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The orthographic map puts a point where its frame says it lies on paper, to 1e-9 mm, for a
|
||||
/// turned frame, a scale and a shift: the guarantee that a picture and lines drawn from the same
|
||||
/// frame register.
|
||||
#[test]
|
||||
fn the_orthographic_map_is_the_frames_paper_map() {
|
||||
let basis = Plane::new(Vec3::new(3.0, -2.0, 7.0), Vec3::new(0.8, 0.6, 0.0), Vec3::new(-0.3, 0.4, 0.9)).unwrap();
|
||||
let frame = OrthoFrame { basis, scale: 0.5, shift: Vec2::new(41.0, 17.5) };
|
||||
let grid = PixelGrid::new(12.5, 4.0, 60.0, 30.0, 300.0).unwrap();
|
||||
let scene = opaque_and_glass();
|
||||
let view = Projection::Ortho { frame, grid }.view(&scene, 2).unwrap();
|
||||
let (x0, y0, x1, y1) = grid.covers();
|
||||
for mesh in &scene.meshes {
|
||||
for p in &mesh.mesh.positions {
|
||||
let c = view.vp.apply(*p);
|
||||
assert_eq!(c[3], 1.0);
|
||||
let paper = Vec2::new(x0 + (c[0] + 1.0) / 2.0 * (x1 - x0), y0 + (c[1] + 1.0) / 2.0 * (y1 - y0));
|
||||
let want = frame.to_paper(*p);
|
||||
assert!((paper - want).len() < 1e-9, "{p:?}: {paper:?} against {want:?}");
|
||||
assert!((-1e-12..=1.0 + 1e-12).contains(&c[2]), "depth within the design's box: {c:?}");
|
||||
}
|
||||
}
|
||||
// Nearer the viewer (along the normal) is nearer in depth.
|
||||
let n = basis.normal();
|
||||
assert!(view.vp.apply(basis.origin + n)[2] < view.vp.apply(basis.origin)[2]);
|
||||
}
|
||||
|
||||
/// A depth-only body hides what lies behind it and draws nothing itself, whichever is drawn
|
||||
/// first; the painted body shows where it is not covered.
|
||||
#[test]
|
||||
fn a_depth_only_body_hides_what_is_behind_it() {
|
||||
let (frame, grid) = front_view();
|
||||
// Seen from −Y: the front block (y −10) covers paper x 40..50 of the back one (x 40..60).
|
||||
let front = block("Front", [20, 20, 220], Vec3::new(-5.0, -10.0, 0.0), Vec3::new(5.0, 2.0, 5.0));
|
||||
let back = block("Back", [220, 20, 20], Vec3::new(0.0, 10.0, 0.0), Vec3::new(10.0, 2.0, 5.0));
|
||||
for order in [[0, 1], [1, 0]] {
|
||||
let bodies = [front.clone(), back.clone()];
|
||||
let meshes: Vec<ProductMesh> = order.iter().map(|&i| bodies[i].clone()).collect();
|
||||
let roles: Vec<BodyRole> = order.iter().map(|&i| if i == 0 { BodyRole::DepthOnly } else { BodyRole::Painted }).collect();
|
||||
let request = PictureRequest {
|
||||
projection: Projection::Ortho { frame, grid },
|
||||
tiles: tiles(100, 50, 1, 1),
|
||||
supersample: Supersample::One,
|
||||
roles,
|
||||
background: None,
|
||||
};
|
||||
let rows = picture(scene(meshes), request);
|
||||
let px = |x: usize, y: usize| <[u8; 4]>::try_from(&rows[(y * 100 + x) * 4..(y * 100 + x) * 4 + 4]).unwrap();
|
||||
assert_eq!(px(45, 25), [0; 4], "order {order:?}: hidden behind the depth-only body, which draws nothing");
|
||||
let shown = px(55, 25);
|
||||
assert!(shown[3] == 255 && shown[0] > shown[2], "order {order:?}: the painted body beside it: {shown:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A picture's pixel work is admitted as a product image's is, by the raster's own projection:
|
||||
/// under a cap that grows with its samples, and counting a depth-only body, which is never see-through.
|
||||
#[test]
|
||||
fn a_picture_is_admitted_under_its_own_pixel_work_cap() {
|
||||
let (frame, grid) = front_view();
|
||||
// Its faces drawn `times` over.
|
||||
let big = |times: usize| {
|
||||
let mut big = block("Big", [100; 3], Vec3::ZERO, Vec3::new(60.0, 2.0, 30.0));
|
||||
let mesh = Arc::make_mut(&mut big.mesh);
|
||||
mesh.triangles = mesh.triangles.repeat(times);
|
||||
mesh.tri_face = mesh.tri_face.repeat(times);
|
||||
scene(vec![big])
|
||||
};
|
||||
let small = PictureRequest {
|
||||
projection: Projection::Ortho { frame, grid },
|
||||
tiles: tiles(100, 50, 1, 1),
|
||||
supersample: Supersample::One,
|
||||
roles: Vec::new(),
|
||||
background: None,
|
||||
};
|
||||
assert!(admit_picture(&big(8000), &small).unwrap().projected_pixel_tests.unwrap() > MAX_PIXEL_TESTS, "over a product image's cap");
|
||||
// The raster holds it to the same figure: it prepares all of that and starts to paint.
|
||||
let mut task = PictureTask::new(big(8000), small.clone(), CancelToken::default()).unwrap();
|
||||
while !matches!(task.step(16_384).unwrap(), PicturePoll::Pending(p) if matches!(p.phase, Phase::Running)) {}
|
||||
|
||||
let sheet = OrthoFrame { shift: Vec2::new(594.0, 420.0), ..frame };
|
||||
let a0_grid = PixelGrid::new(0.0, 0.0, 1189.0, 841.0, 1200.0).unwrap();
|
||||
let a0 = PictureRequest {
|
||||
projection: Projection::Ortho { frame: sheet, grid: a0_grid },
|
||||
tiles: tiles(a0_grid.width, a0_grid.height, 28, 20),
|
||||
..small.clone()
|
||||
};
|
||||
// A larger picture gets no more: the figure bounds the time, whatever the sheet.
|
||||
let error = admit_picture(&big(100), &a0).unwrap_err();
|
||||
assert_eq!(error.code, ErrorCode::LimitExceeded);
|
||||
assert!(error.message.contains("400000000 pixel tests"), "{}", error.message);
|
||||
// A small part of that picture counts only what it draws.
|
||||
let part = PictureRequest { tiles: vec![PixelWindow { x0: 28_000, y0: 19_000, x1: 28_064, y1: 19_064 }], ..a0.clone() };
|
||||
assert!(admit_picture(&big(100), &part).unwrap().projected_pixel_tests.unwrap() <= 100 * 12 * 64 * 64);
|
||||
|
||||
let mut unseen = block("Unseen", [100; 3], Vec3::ZERO, Vec3::new(10.0, 2.0, 5.0));
|
||||
unseen.opacity = 0.0;
|
||||
let count = |request: &PictureRequest| admit_picture(&scene(vec![unseen.clone()]), request).unwrap().projected_pixel_tests.unwrap();
|
||||
assert_eq!(count(&small), 0, "a painted body nothing can see draws nothing");
|
||||
assert!(count(&PictureRequest { roles: vec![BodyRole::DepthOnly], ..small.clone() }) > 0, "a depth-only one hides what is behind it");
|
||||
}
|
||||
|
||||
/// One triangle at paper `corners`, in [`front_view`].
|
||||
fn one_triangle(corners: [[f64; 2]; 3]) -> ProductScene {
|
||||
let positions = corners.iter().map(|&[x, y]| Vec3::new(x - 50.0, 0.0, y - 25.0)).collect();
|
||||
let mesh = Mesh { positions, normals: vec![Vec3::new(0.0, -1.0, 0.0); 3], triangles: vec![[0, 1, 2]], tri_face: vec![0], ..Default::default() };
|
||||
scene(vec![ProductMesh { mesh: Arc::new(mesh), ..block("One", [100; 3], Vec3::ZERO, Vec3::new(1.0, 1.0, 1.0)) }])
|
||||
}
|
||||
|
||||
/// A triangle counts only where it meets the tiles asked for, on the raster's own bounds, and the
|
||||
/// count is the work: a thin one across a tile's edge, one outside, a tile inside a huge one, and a
|
||||
/// long sliver. The numbers are bounds' columns times rows, worked by hand on the 100 x 50 canvas.
|
||||
#[test]
|
||||
fn a_triangle_counts_only_where_it_meets_the_tiles() {
|
||||
let (frame, grid) = front_view();
|
||||
let window = |x0: u32, x1: u32, y0: u32, y1: u32| PixelWindow { x0, y0, x1, y1 };
|
||||
let whole = window(0, 100, 0, 50);
|
||||
let thin = [[29.5, 10.5], [30.5, 10.5], [30.5, 40.5]];
|
||||
let huge = [[-1000.0, -1000.0], [3000.0, -1000.0], [-1000.0, 3000.0]];
|
||||
let sliver = [[0.5, 0.5], [99.5, 49.5], [99.5, 49.6]];
|
||||
let cases = [
|
||||
// Columns 29 to 31, rows 9 to 40: 3 x 32; each side of the edge at 30 has its own part.
|
||||
(thin, vec![whole], 96),
|
||||
(thin, vec![window(0, 30, 0, 50)], 32),
|
||||
(thin, vec![window(30, 100, 0, 50)], 64),
|
||||
(thin, vec![window(0, 30, 0, 50), window(30, 100, 0, 50)], 96),
|
||||
(thin, vec![window(60, 70, 0, 50)], 0),
|
||||
(huge, vec![whole], 5000),
|
||||
(huge, vec![window(10, 20, 10, 20)], 100),
|
||||
(sliver, vec![whole], 5000),
|
||||
(sliver, vec![window(40, 60, 20, 30)], 200),
|
||||
];
|
||||
for (corners, windows, count) in cases {
|
||||
let request = PictureRequest {
|
||||
projection: Projection::Ortho { frame, grid },
|
||||
tiles: windows,
|
||||
supersample: Supersample::One,
|
||||
roles: Vec::new(),
|
||||
background: None,
|
||||
};
|
||||
let drawn = one_triangle(corners);
|
||||
assert_eq!(admit_picture(&drawn, &request).unwrap().projected_pixel_tests, Some(count), "{corners:?}: {:?}", request.tiles);
|
||||
let mut task = PictureTask::new(drawn, request.clone(), CancelToken::default()).unwrap();
|
||||
// Each tile's samples are cleared and put down, a unit each, and the rest are its tests.
|
||||
let held: u64 = request.tiles.iter().map(|t| 2 * (t.width() * t.height()) as u64).sum();
|
||||
loop {
|
||||
match task.step(1).unwrap() {
|
||||
PicturePoll::Complete => break,
|
||||
PicturePoll::Pending(p) if matches!(p.phase, Phase::Running) => assert_eq!(p.total, count + held, "progress is of the tiles"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
assert_eq!(task.raster.pixel_tests, count, "the raster counts what admission did");
|
||||
assert_eq!(task.raster.pixel_done, count + held, "{corners:?}: the count is the work: {:?}", request.tiles);
|
||||
}
|
||||
// At two samples a pixel, a tile holds four times the samples, from its own first one.
|
||||
let two = PictureRequest {
|
||||
projection: Projection::Ortho { frame, grid },
|
||||
tiles: vec![window(10, 20, 10, 20)],
|
||||
supersample: Supersample::Two,
|
||||
roles: Vec::new(),
|
||||
background: None,
|
||||
};
|
||||
assert_eq!(admit_picture(&one_triangle(huge), &two).unwrap().projected_pixel_tests, Some(400));
|
||||
// A sum at its end is refused by name, not wrapped; no one triangle's count can overflow, its
|
||||
// sides being two samples a pixel of at most 65536 pixels.
|
||||
let view = Projection::Ortho { frame, grid }.view(&one_triangle(thin), 1).unwrap();
|
||||
let triangle = view.project(&one_triangle(thin).meshes[0], 0, None, true).unwrap().unwrap();
|
||||
let mut sum = u64::MAX;
|
||||
let e = add_pixel_tests(&mut sum, &triangle, [0, 99, 0, 49], u64::MAX).unwrap_err();
|
||||
assert_eq!(e.message, "pixel work overflow");
|
||||
}
|
||||
|
||||
/// A picture outside its bounds is refused before anything is allocated.
|
||||
#[test]
|
||||
fn hostile_pictures_are_refused() {
|
||||
let (frame, grid) = front_view();
|
||||
let good = PictureRequest {
|
||||
projection: Projection::Ortho { frame, grid },
|
||||
tiles: tiles(100, 50, 1, 1),
|
||||
supersample: Supersample::Two,
|
||||
roles: Vec::new(),
|
||||
background: None,
|
||||
};
|
||||
let code = |request: PictureRequest| PictureTask::new(opaque_and_glass(), request, CancelToken::default()).err().map(|e| e.code);
|
||||
assert_eq!(code(good.clone()), None);
|
||||
let bad = |f: &dyn Fn(&mut PictureRequest)| {
|
||||
let mut request = good.clone();
|
||||
f(&mut request);
|
||||
code(request)
|
||||
};
|
||||
let invalid = Some(ErrorCode::InvalidRequest);
|
||||
let one = vec![PixelWindow { x0: 0, y0: 0, x1: 1, y1: 1 }];
|
||||
assert_eq!(bad(&|r| r.tiles.clear()), invalid, "no tiles");
|
||||
assert_eq!(bad(&|r| r.tiles = vec![PixelWindow { x0: 0, y0: 0, x1: 101, y1: 10 }]), invalid, "a tile outside the picture");
|
||||
assert_eq!(bad(&|r| r.tiles = vec![PixelWindow { x0: 5, y0: 0, x1: 5, y1: 10 }]), invalid, "an empty tile");
|
||||
assert_eq!(bad(&|r| r.tiles = vec![one[0]; MAX_TILES + 1]), invalid, "too many tiles");
|
||||
assert_eq!(bad(&|r| r.roles = vec![BodyRole::Painted]), invalid, "a role for some bodies only");
|
||||
let mut skew = frame;
|
||||
skew.basis.y = Vec3::new(0.0, 0.3, 1.0);
|
||||
assert_eq!(bad(&|r| r.projection = Projection::Ortho { frame: skew, grid }), invalid, "a basis that isn't orthonormal");
|
||||
for scale in [0.0, -1.0, f64::NAN] {
|
||||
assert_eq!(bad(&|r| r.projection = Projection::Ortho { frame: OrthoFrame { scale, ..frame }, grid }), invalid, "scale {scale}");
|
||||
}
|
||||
let far = OrthoFrame { shift: Vec2::new(f64::INFINITY, 0.0), ..frame };
|
||||
assert_eq!(bad(&|r| r.projection = Projection::Ortho { frame: far, grid }), invalid, "an infinite shift");
|
||||
assert_eq!(bad(&|r| r.projection = Projection::Ortho { frame, grid: PixelGrid { pitch: f64::NAN, ..grid } }), invalid, "a NaN pixel");
|
||||
let limit = Some(ErrorCode::LimitExceeded);
|
||||
let wide = Projection::Camera { camera: Camera::default(), width: 65_537, height: 1 };
|
||||
assert_eq!(bad(&|r| (r.projection, r.tiles) = (wide, one.clone())), limit, "a side over 65,536 px");
|
||||
// A whole A0 sheet at 1200 dpi is accepted, drawn in strips; one tile that large is not.
|
||||
let a0 = Projection::Ortho { frame, grid: PixelGrid::new(0.0, 0.0, 1189.0, 841.0, 1200.0).unwrap() };
|
||||
assert_eq!(bad(&|r| (r.projection, r.tiles) = (a0, vec![PixelWindow { x0: 0, y0: 0, x1: 4096, y1: 64 }])), None);
|
||||
// As the product image refuses a size over the resolver's limits.
|
||||
assert_eq!(bad(&|r| (r.projection, r.tiles) = (a0, vec![PixelWindow { x0: 0, y0: 0, x1: 5000, y1: 5000 }])), invalid, "a tile over its limits");
|
||||
}
|
||||
|
||||
@@ -45,7 +45,10 @@ pub use host::{
|
||||
CpuHost, HostKind, ImageHost, JobEnvironmentInfo, JobHandle, JobStatus, LinearAlphaConvention, LinearColourEncoding, LinearMasterInfo,
|
||||
PreviewInfo, RenderFrame, RenderPreview,
|
||||
};
|
||||
pub use image::{Admission, ImageTask, TaskPoll, admit, admit_geometry, admit_geometry_with_limits};
|
||||
pub use image::{
|
||||
Admission, BodyRole, ImageTask, MAX_TILES, OrthoFrame, PicturePoll, PictureRequest, PictureTask, PixelWindow, Projection, Supersample, TaskPoll,
|
||||
admit, admit_geometry, admit_geometry_with_limits, admit_picture,
|
||||
};
|
||||
pub use lens::ThinLens;
|
||||
pub use lighting::{AreaLight, AreaLights, MAX_AREA_LIGHTS, StudioLighting, Sun, ToneMap};
|
||||
pub use png::PngTask;
|
||||
|
||||
Reference in New Issue
Block a user