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,197 @@
|
||||
//! Per-document, transient render ownership. Captured scenes remain immutable when CAD edits
|
||||
//! continue; callers get the captured revision and must never present it as a newer scene.
|
||||
|
||||
use solidcraft_render::{
|
||||
ExecutorDescriptor,
|
||||
job::{CpuHost, ErrorCode, JobError, JobHandle, JobId, JobStatus, Phase, RenderFrame},
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
static NEXT_OWNER: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
fn owner() -> u64 {
|
||||
let mut n = NEXT_OWNER.load(Ordering::Relaxed);
|
||||
loop {
|
||||
let Some(next) = n.checked_add(1) else { return 0 };
|
||||
match NEXT_OWNER.compare_exchange_weak(n, next, Ordering::Relaxed, Ordering::Relaxed) {
|
||||
Ok(_) => return n,
|
||||
Err(current) => n = current,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Jobs {
|
||||
owner: u64,
|
||||
serial: u64,
|
||||
generation: u64,
|
||||
pub(crate) host: Option<CpuHost>,
|
||||
pub(crate) descriptor: Option<ExecutorDescriptor>,
|
||||
live: Vec<JobHandle>,
|
||||
history: VecDeque<JobStatus>,
|
||||
last_good: Option<JobHandle>,
|
||||
}
|
||||
|
||||
impl Default for Jobs {
|
||||
fn default() -> Self {
|
||||
Self { owner: owner(), serial: 0, generation: 0, host: None, descriptor: None, live: Vec::new(), history: VecDeque::new(), last_good: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Jobs {
|
||||
fn drop(&mut self) {
|
||||
for job in &self.live {
|
||||
let _ = job.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Jobs {
|
||||
pub(crate) fn scratch(&self) -> Self {
|
||||
let mut jobs = Self::default();
|
||||
jobs.host = self.host.clone();
|
||||
jobs.descriptor = self.descriptor.clone();
|
||||
jobs
|
||||
}
|
||||
|
||||
pub(crate) fn collect(&mut self) -> Result<(), JobError> {
|
||||
let mut i = 0;
|
||||
while i < self.live.len() {
|
||||
let status = self.live.get(i).ok_or_else(|| JobError::new(ErrorCode::Internal, "missing live job"))?.status()?;
|
||||
if !status.progress.phase.terminal() {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let handle = self.live.remove(i);
|
||||
if status.progress.phase == Phase::Completed && self.last_good.as_ref().is_none_or(|old| old.id().serial < handle.id().serial) {
|
||||
self.last_good = Some(handle);
|
||||
}
|
||||
self.history.push_back(status);
|
||||
while self.history.len() > 4 {
|
||||
self.history.pop_front();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn start(
|
||||
&mut self,
|
||||
revision: u64,
|
||||
scene: solidcraft_render::ProductScene,
|
||||
request: solidcraft_render::job::ImageRequest,
|
||||
replace: bool,
|
||||
) -> Result<JobStatus, JobError> {
|
||||
self.collect()?;
|
||||
let host = self.host.as_ref().ok_or_else(|| JobError::new(ErrorCode::HostUnavailable, "no image host is installed"))?.clone();
|
||||
if replace {
|
||||
for job in self.live.drain(..) {
|
||||
let mut status = job.cancel()?;
|
||||
// Replaced handles are deliberately abandoned: keep a cancellation tombstone,
|
||||
// not a permanently "cancelling" snapshot. The token already prevents any
|
||||
// publication; the host can finish releasing its buffers asynchronously.
|
||||
if !status.progress.phase.terminal() {
|
||||
status.progress.phase = Phase::Cancelled;
|
||||
}
|
||||
self.history.push_back(status);
|
||||
}
|
||||
while self.history.len() > 4 {
|
||||
self.history.pop_front();
|
||||
}
|
||||
}
|
||||
if self.live.len() >= 2 {
|
||||
return Err(JobError::new(ErrorCode::QueueFull, "this document already has an active and pending render"));
|
||||
}
|
||||
if self.owner == 0 {
|
||||
return Err(JobError::new(ErrorCode::LimitExceeded, "render session identity exhausted"));
|
||||
}
|
||||
let serial = self.serial.checked_add(1).ok_or_else(|| JobError::new(ErrorCode::LimitExceeded, "render job identity exhausted"))?;
|
||||
let generation = self.generation.checked_add(1).ok_or_else(|| JobError::new(ErrorCode::LimitExceeded, "render generation exhausted"))?;
|
||||
let handle = host.submit(JobId { session: self.owner, serial, generation }, revision, scene, request)?;
|
||||
let status = handle.status()?;
|
||||
self.serial = serial;
|
||||
self.generation = generation;
|
||||
self.live.push(handle);
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
fn check_owner(&self, id: JobId) -> Result<(), JobError> {
|
||||
if id.session != self.owner {
|
||||
return Err(JobError::new(ErrorCode::WrongOwner, "render job belongs to another document session"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn handle(&self, id: JobId) -> Result<JobHandle, JobError> {
|
||||
self.check_owner(id)?;
|
||||
self.live
|
||||
.iter()
|
||||
.chain(self.last_good.iter())
|
||||
.find(|j| j.id() == id)
|
||||
.cloned()
|
||||
.ok_or_else(|| JobError::new(ErrorCode::UnknownJob, "render job is no longer retained"))
|
||||
}
|
||||
|
||||
pub(crate) fn status(&mut self, id: JobId) -> Result<JobStatus, JobError> {
|
||||
self.check_owner(id)?;
|
||||
self.collect()?;
|
||||
if let Ok(handle) = self.handle(id) {
|
||||
return handle.status();
|
||||
}
|
||||
self.history.iter().find(|s| s.job == id).cloned().ok_or_else(|| JobError::new(ErrorCode::UnknownJob, "render job is no longer retained"))
|
||||
}
|
||||
|
||||
pub(crate) fn frame(&mut self, id: JobId) -> Result<Arc<RenderFrame>, JobError> {
|
||||
self.collect()?;
|
||||
self.handle(id)?.frame()?.ok_or_else(|| JobError::new(ErrorCode::NotCompleted, "render has no completed image"))
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Session {
|
||||
/// A host is installed by the application, never by document data or a command.
|
||||
pub fn set_render_job_host(&mut self, host: Option<CpuHost>) -> crate::Result<()> {
|
||||
if self.render_jobs.host.as_ref().zip(host.as_ref()).is_some_and(|(a, b)| a.same_host(b)) {
|
||||
return Ok(());
|
||||
}
|
||||
let descriptor = host.as_ref().map(CpuHost::descriptor).transpose().map_err(job_error)?;
|
||||
let mut backend = self.render_backend();
|
||||
backend.executors.retain(|e| self.render_jobs.descriptor.as_ref().is_none_or(|old| old.id != e.id));
|
||||
if let Some(d) = &descriptor {
|
||||
backend.executors.push(d.clone());
|
||||
}
|
||||
solidcraft_render::resolve_request(
|
||||
&[backend],
|
||||
&solidcraft_render::RenderRequest {
|
||||
backend: None,
|
||||
mode: solidcraft_render::RenderMode::Preview,
|
||||
operation: solidcraft_render::RenderOperation::Image,
|
||||
width: 1,
|
||||
height: 1,
|
||||
format: Some(solidcraft_render::RenderFormat::Png),
|
||||
features: Vec::new(),
|
||||
policy: Default::default(),
|
||||
},
|
||||
)
|
||||
.map_err(|e| crate::EngineError::Other(e.to_string()))?;
|
||||
let mut jobs = Jobs::default();
|
||||
jobs.host = host;
|
||||
jobs.descriptor = descriptor;
|
||||
self.render_jobs = jobs;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_job_host(&self) -> Option<&CpuHost> {
|
||||
self.render_jobs.host.as_ref()
|
||||
}
|
||||
|
||||
/// Explicit native CLI waiting is separate from non-blocking status commands.
|
||||
pub fn render_job_handle(&self, id: JobId) -> crate::Result<JobHandle> {
|
||||
self.render_jobs.handle(id).map_err(job_error)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn job_error(error: JobError) -> crate::EngineError {
|
||||
crate::EngineError::Other(error.to_string())
|
||||
}
|
||||
Reference in new issue
Block a user