Render: integrated workspace, preview image jobs and opaque PBR (#1046)
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>
This commit is contained in:
1 parent
e4ef6cfe29
commit
a261aaa841
70 files changed
+5860
-101
No files matched your search
@@ -0,0 +1,131 @@
|
||||
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);
|
||||
}
|
||||
Reference in new issue
Block a user