use crate::{
bridge::frame_cache::stone_width::StoneWidthCache,
settings::model::{LiveComputeTarget, LocalComputeTarget, LocalPreviewScale},
};
use glam::Vec3;
use indicatrix::{
geometry::{cuts::StandardGemCuts, plane::GpuFacetPlane},
optics::{
materials::{GemMaterial, OpticalCharacter},
raytracer::LightingPreset,
},
renderer::env_map::{EnvMapError, EnvironmentMap},
};
use indicatrix_net::client::Accumulator;
use std::sync::{Arc, Mutex};
pub struct RenderContext {
pub width: u32,
pub height: u32,
pub yaw: f32,
pub pitch: f32,
pub distance: f32,
pub light_yaw: f32,
pub light_pitch: f32,
pub material_name: String,
pub material_override: Option<GemMaterial>,
pub lighting_preset: LightingPreset,
pub backdrop: crate::settings::model::Backdrop,
pub target_samples: u32,
pub max_bounces: u32,
pub exposure: f32,
pub inclusion_sigma_s: f32,
pub c_axis_override: Option<Vec3>,
pub girdle_frosted: bool,
pub edge_rounding_radius: f32,
pub stone_width_mm: f32,
pub active_planes: Arc<Vec<GpuFacetPlane>>,
pub design_gear: Option<(u32, f32)>,
pub planes_owner: PlanesOwner,
pub material_unresolved: Option<String>,
pub custom_materials: Arc<Vec<GemMaterial>>,
pub running: bool,
pub dirty: bool,
pub paused: bool,
pub tab_visible: bool,
pub denoise_enabled: bool,
pub remote_active: bool,
pub remote_accumulator: Option<Arc<Mutex<Accumulator>>>,
pub remote_reserved_samples: u32,
pub export_active: bool,
pub remote_render_samples: u32,
pub live_compute_target: LiveComputeTarget,
pub local_compute_target: LocalComputeTarget,
pub local_preview_scale: LocalPreviewScale,
pub camera_moving: bool,
pub env_map: Option<Arc<EnvironmentMap>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PlanesOwner {
#[default]
Builtin,
Editor {
generation: u64,
},
Catalogue {
entry_id: i64,
},
}
impl RenderContext {
#[must_use]
pub const fn may_claim_active_planes(&self, new_owner: PlanesOwner) -> bool {
match (self.planes_owner, new_owner) {
(
PlanesOwner::Editor {
generation: current,
},
PlanesOwner::Editor { generation: new },
) => new >= current,
(PlanesOwner::Editor { .. }, _) => false,
_ => true,
}
}
pub fn claim_active_planes(
&mut self,
planes: Arc<Vec<GpuFacetPlane>>,
design_gear: Option<(u32, f32)>,
owner: PlanesOwner,
) -> bool {
if !self.may_claim_active_planes(owner) {
return false;
}
self.active_planes = planes;
self.design_gear = design_gear;
self.planes_owner = owner;
true
}
#[must_use]
pub const fn traced_planes_are_stale(&self, current_generation: u64) -> bool {
matches!(
self.planes_owner,
PlanesOwner::Editor { generation } if generation != current_generation
)
}
}
impl Default for RenderContext {
fn default() -> Self {
Self {
width: 800,
height: 600,
yaw: 0.60, pitch: 0.45, distance: 2.4,
light_yaw: 0.85, light_pitch: 0.95, material_name: "Diamond".to_string(),
material_override: None,
lighting_preset: LightingPreset::RingLights,
backdrop: crate::settings::model::Backdrop::default(),
target_samples: 256,
max_bounces: 12,
exposure: 1.0,
inclusion_sigma_s: 0.0,
c_axis_override: None,
girdle_frosted: false,
edge_rounding_radius: 0.0,
stone_width_mm: 0.0,
active_planes: Arc::new(StandardGemCuts::standard_round_brilliant()),
design_gear: None,
planes_owner: PlanesOwner::Builtin,
material_unresolved: None,
custom_materials: Arc::new(Vec::new()),
running: true,
dirty: true,
paused: false,
tab_visible: true,
denoise_enabled: true,
remote_active: false,
remote_accumulator: None,
remote_reserved_samples: 0,
export_active: false,
remote_render_samples: 512,
live_compute_target: LiveComputeTarget::Both,
local_compute_target: LocalComputeTarget::CpuGpu,
local_preview_scale: LocalPreviewScale::Off,
camera_moving: false,
env_map: None,
}
}
}
pub fn load_env_map(path: &str) -> Result<Arc<EnvironmentMap>, String> {
EnvironmentMap::from_hdr_file(path)
.map(Arc::new)
.map_err(|e: EnvMapError| e.to_string())
}
pub(super) struct FrameInputs {
pub(super) width: u32,
pub(super) height: u32,
pub(super) yaw: f32,
pub(super) pitch: f32,
pub(super) distance: f32,
pub(super) light_yaw: f32,
pub(super) light_pitch: f32,
pub(super) material_name: String,
pub(super) material_override: Option<GemMaterial>,
pub(super) material_unresolved: Option<String>,
pub(super) lighting_preset: LightingPreset,
pub(super) backdrop: crate::settings::model::Backdrop,
pub(super) target_samples: u32,
pub(super) max_bounces: u32,
pub(super) exposure: f32,
pub(super) inclusion_sigma_s: f32,
pub(super) c_axis_override: Option<Vec3>,
pub(super) girdle_frosted: bool,
pub(super) edge_rounding_radius: f32,
pub(super) stone_width_mm: f32,
pub(super) active_planes: Arc<Vec<GpuFacetPlane>>,
pub(super) custom_materials: Arc<Vec<GemMaterial>>,
pub(super) running: bool,
pub(super) dirty: bool,
pub(super) paused: bool,
pub(super) tab_visible: bool,
pub(super) denoise_enabled: bool,
pub(super) remote_active: bool,
pub(super) remote_accumulator: Option<Arc<Mutex<Accumulator>>>,
pub(super) remote_reserved_samples: u32,
pub(super) export_active: bool,
pub(super) live_compute_target: LiveComputeTarget,
pub(super) local_compute_target: LocalComputeTarget,
pub(super) local_preview_scale: LocalPreviewScale,
pub(super) camera_moving: bool,
pub(super) env_map: Option<Arc<EnvironmentMap>>,
}
pub(super) fn snapshot_frame_inputs(ctx: &Arc<Mutex<RenderContext>>) -> FrameInputs {
let mut ctx = ctx
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let dirty = ctx.dirty;
ctx.dirty = false;
FrameInputs {
width: ctx.width,
height: ctx.height,
yaw: ctx.yaw,
pitch: ctx.pitch,
distance: ctx.distance,
light_yaw: ctx.light_yaw,
light_pitch: ctx.light_pitch,
material_name: ctx.material_name.clone(),
material_override: ctx.material_override.clone(),
material_unresolved: ctx.material_unresolved.clone(),
lighting_preset: ctx.lighting_preset,
backdrop: ctx.backdrop,
target_samples: ctx.target_samples,
max_bounces: ctx.max_bounces,
exposure: ctx.exposure,
inclusion_sigma_s: ctx.inclusion_sigma_s,
c_axis_override: ctx.c_axis_override,
girdle_frosted: ctx.girdle_frosted,
edge_rounding_radius: ctx.edge_rounding_radius,
stone_width_mm: ctx.stone_width_mm,
active_planes: Arc::clone(&ctx.active_planes),
custom_materials: Arc::clone(&ctx.custom_materials),
running: ctx.running,
dirty,
paused: ctx.paused,
tab_visible: ctx.tab_visible,
denoise_enabled: ctx.denoise_enabled,
remote_active: ctx.remote_active,
remote_accumulator: ctx.remote_accumulator.clone(),
remote_reserved_samples: ctx.remote_reserved_samples,
export_active: ctx.export_active,
live_compute_target: ctx.live_compute_target,
local_compute_target: ctx.local_compute_target,
local_preview_scale: ctx.local_preview_scale,
camera_moving: ctx.camera_moving,
env_map: ctx.env_map.clone(),
}
}
pub fn resolve_material(
materials: &[GemMaterial],
custom_materials: &[GemMaterial],
material_name: &str,
) -> GemMaterial {
custom_materials
.iter()
.find(|m| m.name.eq_ignore_ascii_case(material_name))
.or_else(|| {
materials
.iter()
.find(|m| m.name.eq_ignore_ascii_case(material_name))
})
.cloned()
.unwrap_or_else(|| materials[0].clone())
}
#[must_use]
pub fn resolve_material_with_override(
materials: &[GemMaterial],
custom_materials: &[GemMaterial],
material_override: Option<&GemMaterial>,
material_name: &str,
) -> GemMaterial {
material_override
.cloned()
.unwrap_or_else(|| resolve_material(materials, custom_materials, material_name))
}
#[derive(Clone, Copy)]
pub struct MaterialOverrides {
pub inclusion_sigma_s: f32,
pub c_axis_override: Option<Vec3>,
pub edge_rounding_radius: f32,
pub stone_width_mm: f32,
}
#[must_use]
pub fn apply_material_overrides(
material: GemMaterial,
overrides: &MaterialOverrides,
active_planes: &[GpuFacetPlane],
width_cache: &mut StoneWidthCache,
) -> GemMaterial {
let material = if overrides.inclusion_sigma_s > 0.0 {
material.with_scattering_amount(overrides.inclusion_sigma_s)
} else {
material
};
let mut material = material;
if let Some(axis) = overrides.c_axis_override
&& material.optical_character != OpticalCharacter::Isotropic
{
material.c_axis = axis;
}
let material = if overrides.edge_rounding_radius > 0.0 {
material.with_edge_rounding(overrides.edge_rounding_radius)
} else {
material
};
if overrides.stone_width_mm > 0.0
&& let Some(model_width) = width_cache.ensure(active_planes)
&& model_width > 1e-9
{
let scale = (f64::from(overrides.stone_width_mm) / model_width) as f32;
if scale.is_finite() && scale > 0.0 {
return material.with_absorption_path_scale(scale);
}
}
material
}
pub struct MaterialSources<'a> {
pub materials: &'a [GemMaterial],
pub custom_materials: &'a [GemMaterial],
pub material_override: Option<&'a GemMaterial>,
pub material_name: &'a str,
}
pub(super) fn resolve_material_and_quality(
sources: &MaterialSources<'_>,
target_samples: u32,
overrides: &MaterialOverrides,
active_planes: &[GpuFacetPlane],
width_cache: &mut StoneWidthCache,
) -> (GemMaterial, u32) {
let current_mat = resolve_material_with_override(
sources.materials,
sources.custom_materials,
sources.material_override,
sources.material_name,
);
let current_mat = apply_material_overrides(current_mat, overrides, active_planes, width_cache);
let spp = (target_samples / 64).clamp(1, 8);
(current_mat, spp)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtin_is_the_default_owner_and_anything_may_claim_over_it() {
let ctx = RenderContext::default();
assert_eq!(ctx.planes_owner, PlanesOwner::Builtin);
assert!(ctx.may_claim_active_planes(PlanesOwner::Catalogue { entry_id: 1 }));
assert!(ctx.may_claim_active_planes(PlanesOwner::Editor { generation: 0 }));
}
#[test]
fn a_catalogue_claim_never_overwrites_an_editor_owner() {
let mut ctx = RenderContext::default();
assert!(ctx.claim_active_planes(
Arc::new(Vec::new()),
None,
PlanesOwner::Editor { generation: 3 },
));
assert!(
!ctx.may_claim_active_planes(PlanesOwner::Catalogue { entry_id: 42 }),
"a catalogue click must not silently steal the slot from the editor \
(the exact scenario CAD audit item 58 describes)"
);
assert_eq!(ctx.planes_owner, PlanesOwner::Editor { generation: 3 });
}
#[test]
fn a_stale_editor_generation_never_overwrites_a_newer_one() {
let mut ctx = RenderContext::default();
assert!(ctx.claim_active_planes(
Arc::new(Vec::new()),
None,
PlanesOwner::Editor { generation: 5 },
));
assert!(!ctx.claim_active_planes(
Arc::new(Vec::new()),
None,
PlanesOwner::Editor { generation: 2 },
));
assert_eq!(ctx.planes_owner, PlanesOwner::Editor { generation: 5 });
}
#[test]
fn a_newer_or_equal_editor_generation_may_overwrite_the_current_one() {
let mut ctx = RenderContext::default();
assert!(ctx.claim_active_planes(
Arc::new(Vec::new()),
None,
PlanesOwner::Editor { generation: 5 },
));
assert!(ctx.claim_active_planes(
Arc::new(Vec::new()),
None,
PlanesOwner::Editor { generation: 5 },
));
assert!(ctx.claim_active_planes(
Arc::new(Vec::new()),
None,
PlanesOwner::Editor { generation: 6 },
));
assert_eq!(ctx.planes_owner, PlanesOwner::Editor { generation: 6 });
}
#[test]
fn two_catalogue_claims_freely_replace_each_other() {
let mut ctx = RenderContext::default();
assert!(ctx.claim_active_planes(
Arc::new(Vec::new()),
None,
PlanesOwner::Catalogue { entry_id: 1 },
));
assert!(ctx.claim_active_planes(
Arc::new(Vec::new()),
None,
PlanesOwner::Catalogue { entry_id: 2 },
));
assert_eq!(ctx.planes_owner, PlanesOwner::Catalogue { entry_id: 2 });
}
#[test]
fn traced_planes_match_the_generation_they_were_claimed_at() {
let mut ctx = RenderContext::default();
assert!(ctx.claim_active_planes(
Arc::new(Vec::new()),
None,
PlanesOwner::Editor { generation: 7 },
));
assert!(!ctx.traced_planes_are_stale(7));
}
#[test]
fn traced_planes_are_stale_once_a_newer_edit_has_landed() {
let mut ctx = RenderContext::default();
assert!(ctx.claim_active_planes(
Arc::new(Vec::new()),
None,
PlanesOwner::Editor { generation: 7 },
));
assert!(ctx.traced_planes_are_stale(8));
}
#[test]
fn a_slot_the_editor_has_never_owned_is_never_stale() {
let ctx = RenderContext::default();
assert_eq!(ctx.planes_owner, PlanesOwner::Builtin);
assert!(!ctx.traced_planes_are_stale(1));
let mut ctx = RenderContext::default();
assert!(ctx.claim_active_planes(
Arc::new(Vec::new()),
None,
PlanesOwner::Catalogue { entry_id: 3 },
));
assert!(!ctx.traced_planes_are_stale(1));
}
#[test]
fn a_refused_claim_leaves_every_field_untouched() {
let mut ctx = RenderContext::default();
let original_planes = Arc::new(StandardGemCuts::emerald_cut());
ctx.active_planes = Arc::clone(&original_planes);
ctx.design_gear = Some((96, 0.5));
ctx.planes_owner = PlanesOwner::Editor { generation: 10 };
let accepted = ctx.claim_active_planes(
Arc::new(StandardGemCuts::standard_round_brilliant()),
Some((64, 0.0)),
PlanesOwner::Catalogue { entry_id: 7 },
);
assert!(!accepted);
assert!(Arc::ptr_eq(&ctx.active_planes, &original_planes));
assert_eq!(ctx.design_gear, Some((96, 0.5)));
assert_eq!(ctx.planes_owner, PlanesOwner::Editor { generation: 10 });
}
#[test]
fn no_override_falls_through_to_the_plain_by_name_lookup() {
let materials = GemMaterial::all_materials();
let by_name = resolve_material(&materials, &[], "Diamond");
let via_override_fn = resolve_material_with_override(&materials, &[], None, "Diamond");
assert_eq!(by_name.name, via_override_fn.name);
assert_eq!(by_name.dispersion, via_override_fn.dispersion);
}
#[test]
fn an_override_wins_regardless_of_what_material_name_says() {
let materials = GemMaterial::all_materials();
let quartz = GemMaterial::new_custom("Quartz-ish", 1.5442, 0.013, 0.0, [0.0, 0.0, 0.0]);
let resolved = resolve_material_with_override(
&materials,
&[],
Some(&quartz),
"Diamond", );
assert_eq!(resolved.name, quartz.name);
assert_eq!(resolved.dispersion, quartz.dispersion);
}
}