use super::{overlay::difference_overlay, session::CompareSide};
use crate::{
bridge::preview_render::render_rgba_at_pose,
gui::solid_preview::{
mesh_cache::MeshCache,
preview_state::CameraPose,
raster::{SolidRasterizer, SolidStyle},
to_pixel_buffer,
},
};
use glam::Vec3;
use indicatrix::{
geometry::{plane::GpuFacetPlane, stone_metrics::SolidMesh},
optics::{
materials::GemMaterial,
raytracer::{Camera, DEFAULT_FOV_DEG, DEFAULT_MAX_BOUNCES},
},
};
use slint::{Rgba8Pixel, SharedPixelBuffer};
use std::sync::{
Arc,
atomic::{AtomicU64, Ordering},
};
use tracing::error;
pub(super) const TRACED_SPP: u32 = 48;
pub(super) const TRACED_MAX_EDGE: u32 = 384;
const TRACED_MAX_BOUNCES: u32 = DEFAULT_MAX_BOUNCES;
const MAX_SOLID_EDGE: u32 = 4096;
const SOLID_BACKGROUND: [u8; 4] = [0x12, 0x14, 0x1c, 0xff];
const PLACEHOLDER_DARK: [u8; 4] = SOLID_BACKGROUND;
const PLACEHOLDER_LIGHT: [u8; 4] = [0x25, 0x2b, 0x3b, 0xff];
const PLACEHOLDER_PERIOD: u32 = 10;
pub(super) type Pixels = SharedPixelBuffer<Rgba8Pixel>;
#[must_use]
pub(super) fn solid_style(preform_planes: usize) -> SolidStyle {
SolidStyle {
background: SOLID_BACKGROUND,
preform_plane_count: preform_planes,
..SolidStyle::default()
}
}
fn camera_for(pose: CameraPose) -> Camera {
Camera::new(pose.yaw, pose.pitch, pose.distance, DEFAULT_FOV_DEG)
}
pub(super) struct SolidSideRenderer {
cache: MeshCache,
raster: SolidRasterizer,
}
impl Default for SolidSideRenderer {
fn default() -> Self {
Self {
cache: MeshCache::default(),
raster: SolidRasterizer::new(1, 1),
}
}
}
impl SolidSideRenderer {
pub(super) fn render(
&mut self,
planes: &[(Vec3, f32)],
preform_planes: usize,
pose: CameraPose,
size: (u32, u32),
) -> bool {
self.raster.resize(size.0.max(1), size.1.max(1));
let camera = camera_for(pose);
let style = solid_style(preform_planes);
if let Some(cached) = self.cache.get_or_build(planes) {
self.raster.render_prepared(cached, &camera, &style);
true
} else {
self.raster.render(&SolidMesh::default(), &camera, &style);
false
}
}
}
#[cfg(test)]
#[must_use]
pub(super) fn render_solid_rgba(
planes: &[(Vec3, f32)],
preform_planes: usize,
pose: CameraPose,
size: (u32, u32),
) -> Vec<u8> {
let mut renderer = SolidSideRenderer::default();
renderer.render(planes, preform_planes, pose, size);
renderer.raster.color
}
#[cfg(test)]
#[must_use]
pub(super) fn render_solid_pick(
planes: &[(Vec3, f32)],
preform_planes: usize,
pose: CameraPose,
size: (u32, u32),
) -> Vec<u32> {
let mut renderer = SolidSideRenderer::default();
renderer.render(planes, preform_planes, pose, size);
renderer.raster.pick
}
#[must_use]
pub(super) fn traced_size(size: (u32, u32)) -> (u32, u32) {
let (width, height) = (size.0.max(1), size.1.max(1));
let longest = width.max(height);
if longest <= TRACED_MAX_EDGE {
return (width, height);
}
let scale = f64::from(TRACED_MAX_EDGE) / f64::from(longest);
let scaled = |edge: u32| ((f64::from(edge) * scale).round() as u32).max(1);
(scaled(width), scaled(height))
}
#[must_use]
pub(super) fn pixel_size(width: f32, height: f32, scale_factor: f32) -> (u32, u32) {
let edge = |logical: f32| {
let physical = (logical * scale_factor).round();
if physical.is_finite() && physical > 0.0 {
(physical as u32).min(MAX_SOLID_EDGE)
} else {
0
}
};
(edge(width), edge(height))
}
#[must_use]
pub(super) fn render_traced_rgba(
planes: &[(Vec3, f32)],
material: &GemMaterial,
pose: CameraPose,
size: (u32, u32),
) -> Vec<u8> {
let gpu_planes: Vec<GpuFacetPlane> = planes
.iter()
.map(|&(normal, offset)| GpuFacetPlane::new(normal, -offset))
.collect();
render_rgba_at_pose(
&gpu_planes,
material,
(pose.yaw, pose.pitch, pose.distance),
size,
TRACED_SPP,
TRACED_MAX_BOUNCES,
)
}
#[must_use]
pub(super) fn placeholder_rgba(size: (u32, u32)) -> Vec<u8> {
let (width, height) = (size.0.max(1), size.1.max(1));
let mut rgba = Vec::with_capacity((width as usize) * (height as usize) * 4);
for y in 0..height {
for x in 0..width {
let stripe = ((x + y) / PLACEHOLDER_PERIOD).is_multiple_of(2);
rgba.extend_from_slice(if stripe {
&PLACEHOLDER_LIGHT
} else {
&PLACEHOLDER_DARK
});
}
}
rgba
}
fn rgba_to_pixels(rgba: &[u8], size: (u32, u32)) -> Pixels {
let (width, height) = (size.0.max(1), size.1.max(1));
let expected = (width as usize) * (height as usize) * 4;
let mut buffer = Pixels::new(width, height);
if rgba.len() == expected {
buffer.make_mut_bytes().copy_from_slice(rgba);
} else {
buffer
.make_mut_bytes()
.copy_from_slice(&placeholder_rgba((width, height)));
}
buffer
}
#[derive(Clone)]
pub(super) struct SideGeometry {
planes: Vec<(Vec3, f32)>,
preform_planes: usize,
material: Option<GemMaterial>,
}
impl SideGeometry {
#[must_use]
pub(super) fn from_side(side: &CompareSide) -> Self {
Self {
planes: side.planes.clone(),
preform_planes: side.preform_planes,
material: side.material.as_ref().ok().cloned(),
}
}
}
#[derive(Debug, Clone, Copy)]
pub(super) struct ViewRequest {
pub(super) generation: u64,
pub(super) pose: CameraPose,
pub(super) size: (u32, u32),
}
pub(super) enum FrameEvent {
Solid {
before: Pixels,
after: Pixels,
overlay: Vec<u8>,
},
TracedProgress {
side: u8,
},
Traced {
before: Pixels,
after: Pixels,
},
WorkerFailed {
worker: &'static str,
},
}
pub(super) struct FrameMsg {
pub(super) session_id: u64,
pub(super) generation: u64,
pub(super) event: FrameEvent,
}
pub(super) use crate::gui::latest_worker::LatestWorker;
fn report_worker_failure(post: fn(FrameMsg), session_id: u64, worker: &'static str) {
post(FrameMsg {
session_id,
generation: 0,
event: FrameEvent::WorkerFailed { worker },
});
}
fn is_current(latest: &AtomicU64, generation: u64) -> bool {
latest.load(Ordering::Acquire) == generation
}
fn solid_side_pixels(
renderer: &mut SolidSideRenderer,
side: &SideGeometry,
request: ViewRequest,
) -> (Pixels, bool) {
if side.planes.is_empty() {
return (
rgba_to_pixels(&placeholder_rgba(request.size), request.size),
false,
);
}
let closed = renderer.render(
&side.planes,
side.preform_planes,
request.pose,
request.size,
);
(to_pixel_buffer(&renderer.raster), closed)
}
fn solid_overlay(
before: (&SolidSideRenderer, bool),
after: (&SolidSideRenderer, bool),
size: (u32, u32),
) -> Vec<u8> {
if before.1 && after.1 {
difference_overlay(&before.0.raster.pick, &after.0.raster.pick, None, size)
} else {
vec![0u8; (size.0 as usize) * (size.1 as usize) * 4]
}
}
fn traced_side_pixels(side: &SideGeometry, pose: CameraPose, size: (u32, u32)) -> Pixels {
match &side.material {
Some(material) if !side.planes.is_empty() => rgba_to_pixels(
&render_traced_rgba(&side.planes, material, pose, size),
size,
),
_ => rgba_to_pixels(&placeholder_rgba(size), size),
}
}
#[must_use]
pub(super) fn spawn_solid_worker(
session_id: u64,
sides: (SideGeometry, SideGeometry),
latest: Arc<AtomicU64>,
post: fn(FrameMsg),
) -> LatestWorker<ViewRequest> {
let (before_side, after_side) = sides;
let mut before_renderer = SolidSideRenderer::default();
let mut after_renderer = SolidSideRenderer::default();
LatestWorker::spawn_with_panic_hook(
"compare-solid",
move |request: ViewRequest| {
if !is_current(&latest, request.generation) {
return;
}
let (before, before_solid) =
solid_side_pixels(&mut before_renderer, &before_side, request);
let (after, after_solid) = solid_side_pixels(&mut after_renderer, &after_side, request);
let overlay = solid_overlay(
(&before_renderer, before_solid),
(&after_renderer, after_solid),
(before.width(), before.height()),
);
post(FrameMsg {
session_id,
generation: request.generation,
event: FrameEvent::Solid {
before,
after,
overlay,
},
});
},
move || {
error!(
"The solid comparison render of session {session_id} failed; the next view change renders again."
);
report_worker_failure(post, session_id, "solid");
},
)
}
#[must_use]
pub(super) fn spawn_traced_worker(
session_id: u64,
sides: (SideGeometry, SideGeometry),
latest: Arc<AtomicU64>,
post: fn(FrameMsg),
) -> LatestWorker<ViewRequest> {
let (before_side, after_side) = sides;
LatestWorker::spawn_with_panic_hook(
"compare-traced",
move |request: ViewRequest| {
if !is_current(&latest, request.generation) {
return;
}
let size = traced_size(request.size);
let before = traced_side_pixels(&before_side, request.pose, size);
if !is_current(&latest, request.generation) {
return;
}
post(FrameMsg {
session_id,
generation: request.generation,
event: FrameEvent::TracedProgress { side: 2 },
});
let after = traced_side_pixels(&after_side, request.pose, size);
if !is_current(&latest, request.generation) {
return;
}
post(FrameMsg {
session_id,
generation: request.generation,
event: FrameEvent::Traced { before, after },
});
},
move || {
error!(
"The traced comparison render of session {session_id} failed; the next view change renders again."
);
report_worker_failure(post, session_id, "traced");
},
)
}