* Render hotfix: speed up mapped GPU path setup on lavapipe (#12664) * Render: show GPU shader preparation while a job waits * Render: share emitter lookup and show shader preparation in its own window * Render: preserve preparation progress and dialog keys * Render: target the displayed job from shader preparation Cancel * CI: bound the progressive GPU UI test process
This commit is contained in:
1 parent
73e188df52
commit
684a4f4285
8 files changed
+130
-31
No files matched your search
@@ -33,6 +33,12 @@ slow-timeout = { period = "60s" }
|
||||
[profile.ci.junit]
|
||||
path = "junit.xml"
|
||||
|
||||
# Bound the whole test process, including Vulkan host initialization and teardown.
|
||||
# Cold runs can warm multiple pipelines before the first image starts rendering.
|
||||
[[profile.ci.overrides]]
|
||||
filter = "package(=solidcraft-ui-egui) & test(=render_workspace::tests::progressive_image_controls_route_to_the_registered_host)"
|
||||
slow-timeout = { period = "60s", terminate-after = 20 }
|
||||
|
||||
# The action matrix sample runs its designs in parallel workers (ACTION_MATRIX_JOBS, set to match in
|
||||
# ci.yml). Its workers reserve that many of nextest's threads, so the cores are not oversubscribed
|
||||
# (a pi-case hole that takes 8 s alone went past the matrix's 90 s action limit when they were),
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"ar": {"render.gpu_job_shader_wait": "قد يستغرق تجهيز المظللات دقائق"},
|
||||
"de": {"render.gpu_job_shader_wait": "Shader-Vorbereitung kann Minuten dauern"},
|
||||
"en": {"render.gpu_job_shader_wait": "Shader setup can take minutes"},
|
||||
"he": {"render.gpu_job_shader_wait": "הכנת השיידרים עשויה להימשך כמה דקות"},
|
||||
"ja": {"render.gpu_job_shader_wait": "シェーダーの準備には数分かかることがあります"},
|
||||
"ko": {"render.gpu_job_shader_wait": "셰이더 준비에 몇 분이 걸릴 수 있습니다"},
|
||||
"vi": {"render.gpu_job_shader_wait": "Chuẩn bị shader có thể mất vài phút"},
|
||||
"zh": {"render.gpu_job_shader_wait": "准备着色器可能需要几分钟"}
|
||||
}
|
||||
@@ -1059,7 +1059,7 @@ fn direct_light(t: Triangle, position: vec3<f32>, physical: PrecisePoint, o: vec
|
||||
let emitter_limit=shadow_distance-select(u.lighting.x*2.0,0.0,punctual);
|
||||
if !studio || emitter_hit_precise(shadow_origin,shadow_origin_low,shadow_direction,shadow_minimum).distance>=emitter_limit {
|
||||
visibility = shadow_transmittance(shadow_origin,shadow_origin_low,shadow_direction,shadow_distance,shadow_minimum);
|
||||
if studio && u.volume_min.w>0.0 {
|
||||
if !OPAQUE_SCENE && studio && u.volume_min.w>0.0 {
|
||||
var attenuation=volume_transmittance_precise(shadow_origin,shadow_origin_low,shadow_direction,shadow_distance);
|
||||
if punctual { attenuation=volume_transmittance_precise(physical.hi,physical.lo,sample.direction,physical_distance); }
|
||||
visibility.transmittance*=attenuation;
|
||||
@@ -1174,9 +1174,14 @@ fn sample_path_lane(pixel: u32, lane: u32) -> vec4<f32> {
|
||||
if !OPAQUE_SCENE && u.lighting.w>0.0 && bounce>0u && medium_count==0u && hit.triangle!=0xffffffffu {
|
||||
if !initialize_media_spectral(hit.triangle,d,&media,&medium_count,wavelength) {return medium_failure(radiance,coverage,pixel,bounce,4u);}
|
||||
}
|
||||
if medium_count>0u && media[medium_count-1u].scatter.x>0.0 {
|
||||
// Surface hits and both kinds of scattering share the same ray and
|
||||
// emitter query. Keep one copy of its precise arithmetic in the shader.
|
||||
let emitter = emitter_hit_precise(o,o_low,d,ray_minimum);
|
||||
// Opaque jobs cannot enter a material medium or have a scene volume.
|
||||
// Specialize both walks out before the driver compiles the mapped shader.
|
||||
if !OPAQUE_SCENE && medium_count>0u && media[medium_count-1u].scatter.x>0.0 {
|
||||
let current=media[medium_count-1u];
|
||||
let boundary=min(hit.distance,emitter_hit_precise(o,o_low,d,ray_minimum).distance);
|
||||
let boundary=min(hit.distance,emitter.distance);
|
||||
let travel=-log(1.0-random(&rng))/current.scatter.x;
|
||||
if travel<boundary {
|
||||
texture_path_length+=travel;
|
||||
@@ -1207,8 +1212,8 @@ fn sample_path_lane(pixel: u32, lane: u32) -> vec4<f32> {
|
||||
let segment=ray_segment_precise(o,o_low,previous_position,previous_position_low,d,boundary);
|
||||
previous_pdf*=exp(-current.scatter.x*length(segment));
|
||||
}
|
||||
if u.volume_min.w>0.0 {
|
||||
let emitter_distance=emitter_hit_precise(o,o_low,d,ray_minimum).distance;
|
||||
if !OPAQUE_SCENE && u.volume_min.w>0.0 {
|
||||
let emitter_distance=emitter.distance;
|
||||
var segment=vec2<f32>(0.0);
|
||||
if bounce==0u && u.eye.w<=0.5 {
|
||||
// The orthographic camera has no physical eye plane. Its fog
|
||||
@@ -1253,7 +1258,6 @@ fn sample_path_lane(pixel: u32, lane: u32) -> vec4<f32> {
|
||||
}
|
||||
}
|
||||
if u.environment.w > 0.0 {
|
||||
let emitter = emitter_hit_precise(o,o_low,d,ray_minimum);
|
||||
if emitter.distance < hit.distance {
|
||||
let light = u.area_lights[emitter.index];
|
||||
let segment = ray_segment_precise(o,o_low,previous_position,previous_position_low,d,emitter.distance);
|
||||
|
||||
@@ -513,6 +513,10 @@ impl JobHandle {
|
||||
pub fn id(&self) -> JobId {
|
||||
self.0.id
|
||||
}
|
||||
/// Time since submission, including queueing and preparation.
|
||||
pub fn elapsed(&self) -> std::time::Duration {
|
||||
self.0.started.elapsed()
|
||||
}
|
||||
/// True only after queued/running captures, tasks and their work reservations
|
||||
/// have been dropped. A terminal phase alone does not prove resource release.
|
||||
/// Retained final/preview images and status-input leases remain owned by their
|
||||
|
||||
@@ -188,7 +188,9 @@ fn stage_tools(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw render controls and status above the active model or image viewport.
|
||||
pub(crate) fn overlays(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
crate::render_workspace::show_shader_preparation(app, ui.ctx());
|
||||
crate::shell::register_panel(app, "render:focus:exit", None, None);
|
||||
if focused(app) {
|
||||
dock_chip(app, ui, "render:focus:exit", "render.exit_focus", "view.render_focus", serde_json::json!({"on":false}));
|
||||
|
||||
@@ -3403,6 +3403,38 @@ pub(crate) fn apply_command(app: &mut SolveApp, id: &str, reply: &serde_json::Va
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// GPU setup has no driver progress fraction. Keep its temporary window separate
|
||||
/// from the render progress row and retire it as soon as preparation ends.
|
||||
pub(crate) fn show_shader_preparation(app: &mut SolveApp, ctx: &egui::Context) -> bool {
|
||||
use solidcraft_engine::render::job::Phase;
|
||||
|
||||
let Some(handle) = app.render_workspace.view.job.and_then(|job| app.session.render_job_handle(job).ok()) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(status) = handle.status() else { return false };
|
||||
if status.progress.phase != Phase::Preparing || status.resolved.executor.as_str() != "gpu-path" {
|
||||
return false;
|
||||
}
|
||||
let mut cancel = false;
|
||||
crate::ui_kit::Window::new(egui::Id::unique("render_shader_preparation"), "", app.tr("render.image_preparing")).show_passive_body(ctx, |ui| {
|
||||
let warning = ui.label(app.tr("render.gpu_job_shader_wait"));
|
||||
crate::scenario::publish_rect("render:shader-preparation:warning", warning.rect);
|
||||
ui.horizontal(|ui| {
|
||||
let spinner = ui.spinner();
|
||||
crate::scenario::publish_rect("render:shader-preparation:spinner", spinner.rect);
|
||||
ui.label(mmss(handle.elapsed()));
|
||||
});
|
||||
let button = crate::ui_kit::button(ui, app.tr("cmd.render_cancel"), crate::ui_kit::Button::Normal);
|
||||
crate::scenario::publish_target("render:shader-preparation:cancel", button.rect);
|
||||
cancel = button.clicked();
|
||||
});
|
||||
if cancel && let Err(error) = app.run("render.cancel", json!({"job": handle.id()})) {
|
||||
app.set_status(error, true);
|
||||
}
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(100));
|
||||
true
|
||||
}
|
||||
|
||||
/// `key` names the row's scenario handles (`ui_kit::progress_action`).
|
||||
pub(crate) fn progress_pill(app: &mut SolveApp, ui: &mut egui::Ui, key: &str) -> bool {
|
||||
if prepare::progress(app, ui) {
|
||||
@@ -3435,8 +3467,8 @@ pub(crate) fn progress_pill(app: &mut SolveApp, ui: &mut egui::Ui, key: &str) ->
|
||||
let fraction = (done as f64 / total as f64).clamp(0.0, 1.0) as f32;
|
||||
let phase_key = format!("render.image_{phase}");
|
||||
let name = app.render_workspace.view.job_shot.as_ref().unwrap_or(&app.render_workspace.view.name).clone();
|
||||
let status = format!("· {} · {:.0}%", app.tr(&phase_key), fraction * 100.0);
|
||||
let cancel = crate::ui_kit::progress_action(ui, key, &name, &status, fraction, app.tr_label("Cancel"));
|
||||
let message = format!("· {} · {:.0}%", app.tr(&phase_key), fraction * 100.0);
|
||||
let cancel = crate::ui_kit::progress_action(ui, key, &name, &message, fraction, app.tr_label("Cancel"));
|
||||
crate::scenario::publish_target("render:progress-cancel", cancel.rect);
|
||||
if cancel.clicked() {
|
||||
app.start("render.cancel");
|
||||
|
||||
@@ -1676,17 +1676,11 @@ fn render_preview_tab_surfaces_preserve_scene_backplates_and_viewport_overrides(
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
/// Exercise the registered GPU host through the UI, including mapped output and glass.
|
||||
fn progressive_image_controls_route_to_the_registered_host() {
|
||||
// GPU path tracing can hang on a software adapter (lavapipe); see issue #12664.
|
||||
// Skipped there until that is fixed; it still runs on real GPUs.
|
||||
let software = solidcraft_gpu::select::Policy::from_env()
|
||||
.ok()
|
||||
.and_then(solidcraft_gpu::select::pick)
|
||||
.is_some_and(|(_, report)| report.tier == solidcraft_gpu::select::Tier::Software);
|
||||
if software {
|
||||
eprintln!("skipping progressive UI device test: software GPU adapter (#12664)");
|
||||
return;
|
||||
}
|
||||
use solidcraft_engine::render::job::Phase;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
let host = solidcraft_gpu::select::Policy::from_env()
|
||||
.and_then(|policy| solidcraft_gpu::select::pick(policy).ok_or_else(|| "no qualifying adapter".to_string()))
|
||||
.and_then(|(adapter, _)| solidcraft_gpu::path::host_on_adapter(&adapter).map_err(|e| e.to_string()));
|
||||
@@ -1749,14 +1743,24 @@ fn progressive_image_controls_route_to_the_registered_host() {
|
||||
|
||||
h.step(&json!({"click":{"handle":"render:stage:render.start"}})).unwrap();
|
||||
let job = h.app.render_workspace.view.job.unwrap();
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
||||
let handle = h.app.session.render_job_handle(job).unwrap();
|
||||
// The first mapped render compiles a separate pipeline on a cold Vulkan cache.
|
||||
// Allow the GPU host's setup budget, then keep the short deadline after preparation.
|
||||
let submitted = Instant::now();
|
||||
let queue_deadline = submitted + Duration::from_secs(30);
|
||||
let setup_deadline = submitted + Duration::from_secs(600);
|
||||
let mut render_deadline = None;
|
||||
while h.app.render_workspace.view.completed.is_none() {
|
||||
assert!(
|
||||
std::time::Instant::now() < deadline,
|
||||
"progressive UI job did not complete: {:?}",
|
||||
h.app.session.execute("render.status", &json!({"job":job}))
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
let status = handle.status().unwrap();
|
||||
let now = Instant::now();
|
||||
let deadline = match status.progress.phase {
|
||||
Phase::Queued => queue_deadline,
|
||||
Phase::Preparing => setup_deadline,
|
||||
Phase::Failed | Phase::Cancelled => panic!("progressive UI job ended: {status:?}"),
|
||||
_ => *render_deadline.get_or_insert(now + Duration::from_secs(30)),
|
||||
};
|
||||
assert!(now < deadline, "progressive UI job did not complete: {status:?}");
|
||||
std::thread::sleep(Duration::from_millis(5));
|
||||
h.frames(1);
|
||||
}
|
||||
let status = h.app.session.execute("render.status", &json!({"job":job})).unwrap();
|
||||
@@ -1787,8 +1791,8 @@ fn progressive_image_controls_route_to_the_registered_host() {
|
||||
assert_eq!(image_params(&h.app).unwrap(), checked);
|
||||
let started = h.app.run("render.start", checked).unwrap();
|
||||
let job = serde_json::from_value(started["job"].clone()).unwrap();
|
||||
let status = h.app.session.render_job_handle(job).unwrap().wait_timeout(std::time::Duration::from_secs(30)).unwrap();
|
||||
assert_eq!(status.progress.phase, solidcraft_engine::render::job::Phase::Completed, "{:?}", status.error);
|
||||
let status = h.app.session.render_job_handle(job).unwrap().wait_timeout(Duration::from_secs(30)).unwrap();
|
||||
assert_eq!(status.progress.phase, Phase::Completed, "{:?}", status.error);
|
||||
assert!(Arc::ptr_eq(&doc, &h.app.session.doc));
|
||||
assert_eq!(h.app.session.undo.len(), undo);
|
||||
assert!(!h.app.session.is_dirty());
|
||||
@@ -6975,9 +6979,12 @@ fn auto_opened_image_cancel_returns_to_model_and_preserves_previous_or_explicit_
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn auto_opened_image_failure_returns_to_model_and_reports_the_job_error() {
|
||||
/// Hold a GPU job in preparation to verify the window, shared keys and failure cleanup.
|
||||
fn gpu_preparation_shows_shader_warning_then_reports_job_failure() {
|
||||
use solidcraft_engine::render::job;
|
||||
struct Failed;
|
||||
use std::sync::{Mutex, mpsc};
|
||||
use std::time::Duration;
|
||||
struct Failed(mpsc::Sender<()>, Mutex<mpsc::Receiver<()>>);
|
||||
impl job::ImageExecutor for Failed {
|
||||
fn descriptor(&self) -> job::Result<ExecutorDescriptor> {
|
||||
let mut descriptor = job::CpuHost::cooperative().descriptor()?;
|
||||
@@ -6989,11 +6996,15 @@ fn auto_opened_image_failure_returns_to_model_and_reports_the_job_error() {
|
||||
job::admit(scene, request)
|
||||
}
|
||||
fn begin(&self, _: ProductScene, _: job::ImageRequest, _: job::JobOptions, _: job::CancelToken) -> job::Result<Box<dyn job::ImageWork>> {
|
||||
self.0.send(()).unwrap();
|
||||
let _ = self.1.lock().unwrap().recv_timeout(Duration::from_secs(10));
|
||||
Err(job::JobError::new(job::ErrorCode::Internal, "controlled image failure"))
|
||||
}
|
||||
}
|
||||
let mut h = output_viewer_fixture();
|
||||
let host = job::ImageHost::with_executor(Arc::new(Failed)).unwrap();
|
||||
let (entered, preparing) = mpsc::channel();
|
||||
let (release, wait) = mpsc::channel();
|
||||
let host = job::ImageHost::with_executor(Arc::new(Failed(entered, Mutex::new(wait)))).unwrap();
|
||||
h.app.session.register_render_job_host(host.clone()).unwrap();
|
||||
h.app.render_workspace.path_host = PathHost::Ready(host);
|
||||
h.app.render_workspace.view.method = ImageMethod::GpuRaytrace;
|
||||
@@ -7001,7 +7012,28 @@ fn auto_opened_image_failure_returns_to_model_and_reports_the_job_error() {
|
||||
let job = h.app.render_workspace.view.job.unwrap();
|
||||
assert_eq!(h.app.render_workspace.view.auto_result_job.as_ref().map(|auto| auto.job), Some(job));
|
||||
let handle = h.app.session.render_job_handle(job).unwrap();
|
||||
assert_eq!(handle.wait_timeout(std::time::Duration::from_secs(10)).unwrap().progress.phase, job::Phase::Failed);
|
||||
preparing.recv_timeout(Duration::from_secs(10)).unwrap();
|
||||
assert_eq!(handle.status().unwrap().progress.phase, job::Phase::Preparing);
|
||||
let keys = [egui::Key::Enter, egui::Key::Escape];
|
||||
let events = keys
|
||||
.into_iter()
|
||||
.map(|key| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE })
|
||||
.collect();
|
||||
let _ = h.ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
|
||||
assert!(show_shader_preparation(&mut h.app, ui.ctx()));
|
||||
for key in keys {
|
||||
assert!(ui.ctx().input_mut(|input| input.consume_key(egui::Modifiers::NONE, key)), "{key:?} must be available to other windows");
|
||||
}
|
||||
assert!(progress_pill(&mut h.app, ui, "render:progress"));
|
||||
});
|
||||
assert!(crate::scenario::rect_of("render:progress:name").is_some());
|
||||
assert!(crate::scenario::rect_of("dialog:progress:render:progress").is_some());
|
||||
for control in ["warning", "spinner", "cancel"] {
|
||||
assert!(crate::scenario::rect_of(&format!("render:shader-preparation:{control}")).is_some());
|
||||
}
|
||||
release.send(()).unwrap();
|
||||
assert_eq!(handle.wait_timeout(Duration::from_secs(10)).unwrap().progress.phase, job::Phase::Failed);
|
||||
assert!(!show_shader_preparation(&mut h.app, &h.ctx));
|
||||
cache_render_images(&mut h.app, &h.ctx);
|
||||
assert!(!h.app.render_workspace.view.result);
|
||||
assert!(h.app.render_workspace.view.auto_result_job.is_none());
|
||||
|
||||
@@ -1073,6 +1073,15 @@ impl<'a> Window<'a> {
|
||||
self.fields().actions("", "", None).show(ctx, true, body).0
|
||||
}
|
||||
|
||||
/// Show a temporary status window with no header dismissal or dialog shortcuts.
|
||||
pub(crate) fn show_passive_body(mut self, ctx: &egui::Context, body: impl FnOnce(&mut Ui)) {
|
||||
self.closeable = false;
|
||||
// No footer handles either key, so leave both in the shared input queue.
|
||||
self.enter_submits = true;
|
||||
self.escape_closes = true;
|
||||
self.actions("", "", None).show(ctx, true, body);
|
||||
}
|
||||
|
||||
/// Draw shared header, content and optional footer, retaining the caller's IDs.
|
||||
pub(crate) fn show(self, ctx: &egui::Context, valid: bool, body: impl FnOnce(&mut Ui)) -> (bool, super::Action) {
|
||||
self.surface(ctx, false, |ui| self.content(ui, valid, body, |_| {}))
|
||||
|
||||
Reference in new issue
Block a user