Lands the Render workspace (R3), bounded preview-image jobs with cancellation and atomic PNG export (R4a), and opaque PBR looks as one change. - Render workspace: an overlay-free product preview on the installed GPU device. Returning to Design restores its camera. render.scene and render.settings.apply are added. - Image jobs: render.preflight/start/status/cancel/export run bounded CPU preview jobs. Export captures the job, overwrite choice and document, and checks the caller's file scope before writing. - Opaque PBR: looks can carry metal fraction, roughness and dielectric IOR. The product viewport and preview PNG use them; geometry and mass are unchanged. The v1 catalog export refuses PBR rather than dropping it. - Render setups and PBR looks are saved under design format 21. Labels are in en/de/ja/ko/zh/ru. Supersedes #706 and #800. Refs #620, #673, #756, #1004. Contributed by @0xlillith. Co-authored-by: lillith <35337584+0xlillith@users.noreply.github.com>
132 lines
5.1 KiB
Rust
132 lines
5.1 KiB
Rust
use super::*;
|
|
use solidcraft_geom::{Aabb3, Mesh};
|
|
|
|
fn request() -> ImageRequest {
|
|
ImageRequest { camera: Default::default(), width: 16, height: 16, background: [40, 80, 120], transparent_background: false }
|
|
}
|
|
fn empty() -> ProductScene {
|
|
ProductScene { meshes: Vec::new(), bounds: Aabb3::EMPTY }
|
|
}
|
|
fn id(owner: u64, serial: u64) -> JobId {
|
|
JobId { session: owner, serial, generation: serial }
|
|
}
|
|
fn drain(host: &CpuHost) {
|
|
for _ in 0..10_000 {
|
|
if !host.pump(4096).unwrap() {
|
|
return;
|
|
}
|
|
}
|
|
panic!("small cooperative jobs did not drain");
|
|
}
|
|
|
|
#[test]
|
|
fn queue_is_bounded_and_cancelled_entries_release_their_reservations() {
|
|
let host = CpuHost::cooperative();
|
|
let a = host.submit(id(10, 1), 100, empty(), request()).unwrap();
|
|
let b = host.submit(id(20, 1), 200, empty(), request()).unwrap();
|
|
let before = host.admitted_bytes();
|
|
assert_eq!(host.submit(id(30, 1), 300, empty(), request()).err().unwrap().code, ErrorCode::QueueFull);
|
|
assert_eq!(host.admitted_bytes(), before);
|
|
assert_eq!(a.cancel().unwrap().progress.phase, Phase::Cancelled);
|
|
let c = host.submit(id(30, 1), 300, empty(), request()).unwrap();
|
|
drain(&host);
|
|
assert_eq!(a.status().unwrap().progress.phase, Phase::Cancelled);
|
|
for (handle, owner, generation) in [(&b, 20, 200), (&c, 30, 300)] {
|
|
let frame = handle.frame().unwrap().unwrap();
|
|
assert_eq!(frame.info.job, id(owner, 1));
|
|
assert_eq!(frame.info.scene_generation, generation);
|
|
assert_eq!(frame.info.executor.as_str(), "cpu-product");
|
|
assert_eq!(image::load_from_memory(frame.png()).unwrap().width(), 16);
|
|
}
|
|
assert!(host.admitted_bytes() > 0 && host.admitted_bytes() < before);
|
|
let frame = b.frame().unwrap().unwrap();
|
|
drop(b);
|
|
drop(c);
|
|
assert!(host.admitted_bytes() > 0, "an externally retained frame retains its lease");
|
|
drop(frame);
|
|
assert_eq!(host.admitted_bytes(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn cancellation_while_encoding_cannot_publish_a_late_frame() {
|
|
let host = CpuHost::cooperative();
|
|
let job = host.submit(id(1, 1), 8, empty(), request()).unwrap();
|
|
for _ in 0..10_000 {
|
|
host.pump(64).unwrap();
|
|
let status = job.status().unwrap();
|
|
if status.progress.phase == Phase::Encoding && status.progress.done > 0 {
|
|
break;
|
|
}
|
|
}
|
|
assert_eq!(job.status().unwrap().progress.phase, Phase::Encoding);
|
|
assert_eq!(job.cancel().unwrap().progress.phase, Phase::Cancelling);
|
|
drain(&host);
|
|
assert_eq!(job.status().unwrap().progress.phase, Phase::Cancelled);
|
|
assert!(job.frame().unwrap().is_none());
|
|
assert_eq!(host.admitted_bytes(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn malformed_preparation_fails_the_job_without_stopping_the_host() {
|
|
let host = CpuHost::cooperative();
|
|
let scene = ProductScene {
|
|
bounds: Aabb3::EMPTY,
|
|
meshes: vec![crate::ProductMesh {
|
|
name: "Broken".into(),
|
|
mesh: Arc::new(Mesh { triangles: vec![[99, 99, 99]], ..Default::default() }),
|
|
color: [0; 3],
|
|
opacity: 1.0,
|
|
surface: None,
|
|
faces: Vec::new(),
|
|
}],
|
|
};
|
|
let broken = host.submit(id(1, 1), 1, scene, request()).unwrap();
|
|
drain(&host);
|
|
let status = broken.status().unwrap();
|
|
assert_eq!(status.progress.phase, Phase::Failed);
|
|
assert_eq!(status.error.unwrap().code, ErrorCode::InvalidScene);
|
|
assert!(broken.frame().unwrap().is_none());
|
|
assert!(host.is_available());
|
|
let healthy = host.submit(id(1, 2), 1, empty(), request()).unwrap();
|
|
drain(&host);
|
|
assert_eq!(healthy.status().unwrap().progress.phase, Phase::Completed);
|
|
}
|
|
|
|
#[test]
|
|
fn closing_the_host_cancels_active_and_queued_work() {
|
|
let host = CpuHost::cooperative();
|
|
let budget = host.0.shared.budget.clone();
|
|
let active = host.submit(id(1, 1), 1, empty(), request()).unwrap();
|
|
let queued = host.submit(id(2, 1), 1, empty(), request()).unwrap();
|
|
host.pump(1).unwrap();
|
|
drop(host);
|
|
assert_eq!(active.status().unwrap().progress.phase, Phase::Cancelled);
|
|
assert_eq!(queued.status().unwrap().progress.phase, Phase::Cancelled);
|
|
assert_eq!(budget.used.load(Ordering::Acquire), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn a_foreign_frame_is_rejected_even_if_a_broken_host_puts_it_in_the_result_slot() {
|
|
let host = CpuHost::cooperative();
|
|
let job = host.submit(id(1, 1), 1, empty(), request()).unwrap();
|
|
drain(&host);
|
|
{
|
|
let mut data = job.0.data.lock().unwrap();
|
|
let frame = Arc::get_mut(data.frame.as_mut().unwrap()).unwrap();
|
|
frame.info.job.session = 2;
|
|
}
|
|
assert_eq!(job.frame().err().unwrap().code, ErrorCode::WrongOwner);
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
#[test]
|
|
fn native_job_finishes_without_any_poll_or_ui_pump() {
|
|
let host = CpuHost::for_platform().unwrap();
|
|
assert_eq!(host.kind(), HostKind::NativeWorker);
|
|
let job = host.submit(id(1, 1), 12, empty(), request()).unwrap();
|
|
// A generous deadlock watchdog, not a performance assertion.
|
|
let status = job.wait_timeout(std::time::Duration::from_secs(60)).unwrap();
|
|
assert_eq!(status.progress.phase, Phase::Completed);
|
|
assert_eq!(job.frame().unwrap().unwrap().info.scene_generation, 12);
|
|
}
|