mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
Merge pull request #2368 from ZJHeepfixer/apple-silicon-window-first
EffectCraft — open the window before the GPU compositor is built
This commit is contained in:
@@ -15,6 +15,7 @@ mod screenshot;
|
||||
mod viewer_frames;
|
||||
mod window;
|
||||
|
||||
pub use gpu::{GPU_PREPARING, GpuBuild};
|
||||
#[cfg(test)]
|
||||
pub(crate) use screenshot::{MAX_PENDING_SHOTS, SCREENSHOT_SETTLE_PASSES, SCREENSHOT_SETTLE_S, SCREENSHOT_TIMEOUT_S};
|
||||
|
||||
@@ -74,6 +75,8 @@ impl EffectcraftApp {
|
||||
wgpu: None,
|
||||
gpu: None,
|
||||
gpu_checked: false,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
gpu_build: None,
|
||||
gpu_failures: None,
|
||||
gpu_failure_applied: None,
|
||||
footage_watcher: None,
|
||||
|
||||
@@ -127,7 +127,7 @@ impl EffectcraftApp {
|
||||
self.now = self.clock.now();
|
||||
// The compositor exists before the first paint and while hidden (device-loss handling
|
||||
// must run there too).
|
||||
self.init_gpu(frame);
|
||||
self.init_gpu(ctx, frame);
|
||||
self.apply_gpu_failure(ctx);
|
||||
if self.gpu_failure_applied == Some(true) {
|
||||
self.close_after_device_loss(ctx);
|
||||
@@ -469,6 +469,13 @@ impl EffectcraftApp {
|
||||
self.ui.status.clear();
|
||||
self.toast = None;
|
||||
}
|
||||
// "Preparing the GPU compositor…" stays until the compositor is attached (tens of
|
||||
// seconds on a cold shader cache), not 5 s: `finish_gpu_build` clears it, and the
|
||||
// worker's repaint brings that frame, so nothing needs to tick meanwhile. A click
|
||||
// still dismisses it.
|
||||
if self.ui.status == crate::GPU_PREPARING && self.gpu_initializing() {
|
||||
return;
|
||||
}
|
||||
ui.ctx().request_repaint_after(std::time::Duration::from_millis(500));
|
||||
if !self.ui.status.is_empty() {
|
||||
let since: f64 = ui.data(|d| d.get_temp(egui::Id::unique("status-since")).unwrap_or(now));
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
use super::{Arc, EffectcraftApp, RenderSource, gpu_failure, menus, panels};
|
||||
|
||||
/// The status line while the GPU compositor's worker thread is still building its pipelines
|
||||
/// (see [`EffectcraftApp::init_gpu`]).
|
||||
pub const GPU_PREPARING: &str = "Preparing the GPU compositor… (compositing on the CPU until it is ready)";
|
||||
/// What the compositor's worker thread hands back: the compositor, why the device can't run it,
|
||||
/// or the payload of a panic in the graphics backend.
|
||||
pub type GpuBuild = std::thread::Result<Result<effectcraft_gpu::Gpu, String>>;
|
||||
|
||||
impl EffectcraftApp {
|
||||
pub fn render_source(&self) -> RenderSource {
|
||||
RenderSource {
|
||||
@@ -124,7 +131,19 @@ impl EffectcraftApp {
|
||||
|
||||
/// Set up the GPU compositor on egui-wgpu's device (once). Without an adapter that runs
|
||||
/// compute shaders (WebGL2, software GL) everything stays on the CPU.
|
||||
pub(crate) fn init_gpu(&mut self, frame: &eframe::Frame) {
|
||||
///
|
||||
/// On the desktop the compositor is built on a worker thread. Its compute pipelines (about
|
||||
/// 235 of them) take tens of seconds to compile on a cold shader cache (first launch, after
|
||||
/// an OS update, after an update that changed a shader), and eframe shows the window only
|
||||
/// once the first frame has painted, so building them here kept the window from appearing
|
||||
/// at all. Until the worker is done the viewer composites on the CPU, as it does without a
|
||||
/// GPU, and the status bar says so; the browser has no threads and builds it in place.
|
||||
pub(crate) fn init_gpu(&mut self, ctx: &egui::Context, frame: &eframe::Frame) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.gpu_build.is_some() {
|
||||
self.finish_gpu_build(frame.wgpu_render_state());
|
||||
return;
|
||||
}
|
||||
if self.gpu_checked {
|
||||
return;
|
||||
}
|
||||
@@ -142,10 +161,80 @@ impl EffectcraftApp {
|
||||
info.device_type,
|
||||
if info.driver.is_empty() { String::new() } else { format!(", {} {}", info.driver, info.driver_info) }
|
||||
));
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if self.spawn_gpu_build(ctx, rs) {
|
||||
return;
|
||||
}
|
||||
// In place (the browser, or no thread to be had): nothing to wake later.
|
||||
let _ = ctx;
|
||||
// Backend validation can panic on shader compilation even when device creation
|
||||
// succeeded. Keep the document and the egui renderer, and use CPU compositing.
|
||||
let gpu = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| effectcraft_gpu::Gpu::new(&rs.adapter, rs.device.clone(), rs.queue.clone())));
|
||||
match gpu {
|
||||
let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| effectcraft_gpu::Gpu::new(&rs.adapter, rs.device.clone(), rs.queue.clone())));
|
||||
self.attach_gpu(built, Some(rs));
|
||||
}
|
||||
|
||||
/// Start building the compositor on a `gpu-compositor` worker thread, which hands its result
|
||||
/// back to [`Self::finish_gpu_build`] and wakes the UI. False when no thread could be spawned
|
||||
/// (the caller then builds it in place).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn spawn_gpu_build(&mut self, ctx: &egui::Context, rs: &eframe::egui_wgpu::RenderState) -> bool {
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let (adapter, device, queue) = (rs.adapter.clone(), rs.device.clone(), rs.queue.clone());
|
||||
let gate = self.hooks.before_gpu_build.take();
|
||||
let wake = ctx.clone();
|
||||
let spawned = std::thread::Builder::new().name("gpu-compositor".into()).spawn(move || {
|
||||
// Backend validation can panic on shader compilation even when device creation
|
||||
// succeeded: the panic stays on this thread and arrives as a result.
|
||||
let built = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
if let Some(gate) = gate {
|
||||
gate();
|
||||
}
|
||||
effectcraft_gpu::Gpu::new(&adapter, device, queue)
|
||||
}));
|
||||
// No receiver: the window closed, or the device failed, while this was running.
|
||||
let _ = tx.send(built);
|
||||
wake.request_repaint();
|
||||
});
|
||||
if let Err(e) = spawned {
|
||||
log::warn!("no worker thread for the GPU compositor ({e}); building it on the UI thread");
|
||||
return false;
|
||||
}
|
||||
self.gpu_build = Some(rx);
|
||||
self.session.accel_note = Some(GPU_PREPARING.into());
|
||||
if self.ui.status.is_empty() {
|
||||
self.ui.status = GPU_PREPARING.into();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Take the worker's result when it has arrived ([`Self::init_gpu`]). `rs`: the window's
|
||||
/// device, which a compositor that built is attached to.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn finish_gpu_build(&mut self, rs: Option<&eframe::egui_wgpu::RenderState>) {
|
||||
let Some(rx) = &self.gpu_build else { return };
|
||||
let built = match rx.try_recv() {
|
||||
Ok(built) => built,
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => return,
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||
Err(Box::new("the compositor's worker thread ended without a result") as Box<dyn std::any::Any + Send>)
|
||||
}
|
||||
};
|
||||
self.gpu_build = None;
|
||||
if self.ui.status == GPU_PREPARING {
|
||||
self.ui.status.clear();
|
||||
}
|
||||
// The device failed while the compositor was being built: it stays detached, and
|
||||
// `apply_gpu_failure` has already said why the viewer is on the CPU.
|
||||
if self.gpu_failure_applied.is_some() || self.gpu_failures.as_ref().is_some_and(|bridge| bridge.failure().is_some()) {
|
||||
return;
|
||||
}
|
||||
self.attach_gpu(built, rs);
|
||||
}
|
||||
|
||||
/// Attach a built compositor to the viewer, or record why there is none. Keeps the document
|
||||
/// and the egui renderer either way; without a compositor the viewer composites on the CPU.
|
||||
fn attach_gpu(&mut self, built: GpuBuild, rs: Option<&eframe::egui_wgpu::RenderState>) {
|
||||
match built {
|
||||
Ok(Ok(g)) => {
|
||||
if let Some(bridge) = &self.gpu_failures {
|
||||
bridge.forward_to(g.context().device_failure_notifier());
|
||||
@@ -153,7 +242,12 @@ impl EffectcraftApp {
|
||||
return;
|
||||
}
|
||||
}
|
||||
let Some(rs) = rs else {
|
||||
self.session.accel_note = Some("GPU compositor unavailable: the window has no wgpu device".into());
|
||||
return;
|
||||
};
|
||||
log::info!("GPU compositor: {}", effectcraft_engine::render::Accelerator::name(&g));
|
||||
self.session.accel_note = None;
|
||||
self.session.accel = Some(Arc::new(g.clone()));
|
||||
self.gpu = Some(g);
|
||||
self.wgpu = Some(rs.clone());
|
||||
@@ -170,6 +264,19 @@ impl EffectcraftApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the GPU compositor is still being built on its worker thread (the viewer is on
|
||||
/// the CPU meanwhile). Always false in the browser, which builds it with the first frame.
|
||||
pub fn gpu_initializing(&self) -> bool {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
self.gpu_build.is_some()
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// The viewer shows a frame composited on the GPU (drawn from its wgpu texture).
|
||||
pub fn viewer_on_gpu(&self) -> bool {
|
||||
matches!((&self.viewer_shown, &self.viewer_native), (Some(s), Some(n)) if s.1 == n.1)
|
||||
|
||||
@@ -62,6 +62,10 @@ pub struct Hooks {
|
||||
/// Tells the user the graphics device was lost and the window is closing, once. `saved` is
|
||||
/// the auto-save of unsaved changes (`None`: nothing was unsaved), its path or why it failed.
|
||||
pub device_lost: Option<Box<dyn Fn(Option<Result<String, String>>)>>,
|
||||
/// Runs on the worker thread right before the GPU compositor is built (desktop). Tests hold
|
||||
/// it to look at the interface while the compositor is still being prepared, or make it
|
||||
/// panic to check that a failing build never reaches the UI thread.
|
||||
pub before_gpu_build: Option<Box<dyn FnOnce() + Send>>,
|
||||
}
|
||||
|
||||
impl Hooks {
|
||||
|
||||
@@ -51,6 +51,7 @@ mod app;
|
||||
mod clock;
|
||||
mod hooks;
|
||||
|
||||
pub use app::{GPU_PREPARING, GpuBuild};
|
||||
#[cfg(test)]
|
||||
use app::{MAX_PENDING_SHOTS, SCREENSHOT_SETTLE_PASSES, SCREENSHOT_SETTLE_S, SCREENSHOT_TIMEOUT_S};
|
||||
pub use clock::AppClock;
|
||||
@@ -310,6 +311,10 @@ pub struct EffectcraftApp {
|
||||
/// The GPU compositor on that device (None: no usable adapter → CPU only).
|
||||
pub(crate) gpu: Option<effectcraft_gpu::Gpu>,
|
||||
gpu_checked: bool,
|
||||
/// The GPU compositor being built on a worker thread (desktop): the window opens at once and
|
||||
/// composites on the CPU until the worker's result lands ([`Self::init_gpu`]).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
gpu_build: Option<Receiver<GpuBuild>>,
|
||||
gpu_failures: Option<gpu_failure::GpuFailureBridge>,
|
||||
gpu_failure_applied: Option<bool>,
|
||||
/// Settings ▸ Import ▸ Reload Footage Changed on Disk: the worker that stamps the footage
|
||||
|
||||
@@ -7,6 +7,8 @@ use effectcraft_ui_egui::EffectcraftApp;
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::ui_gpu::wait_for_compositor;
|
||||
|
||||
fn settle(h: &mut Harness<'_, EffectcraftApp>) {
|
||||
for _ in 0..600 {
|
||||
h.step();
|
||||
@@ -50,22 +52,25 @@ fn environment_light_lights_the_scene_on_the_gpu() {
|
||||
eprintln!("headless compositor adapter: {adapter}");
|
||||
let gl = adapter.ends_with("(Gl)");
|
||||
drop(probe);
|
||||
let mut s = Session::default();
|
||||
s.execute("file.openDemoProject", json!({})).unwrap();
|
||||
s.execute("comp.open", json!({"comp": "3D Showcase"})).unwrap();
|
||||
s.execute("time.set", json!({"time": 5.0})).unwrap();
|
||||
s.execute("comp.settings", json!({"renderer": "classic3D"})).unwrap();
|
||||
let mut h = Harness::builder()
|
||||
.with_size(egui::vec2(1400.0, 900.0))
|
||||
.with_pixels_per_point(1.0)
|
||||
.wgpu_setup(same_adapter(adapter))
|
||||
.build_eframe(|_| EffectcraftApp::new(s));
|
||||
settle(&mut h);
|
||||
.build_eframe(|_| EffectcraftApp::new(Session::default()));
|
||||
// The project opens once the compositor (built on a worker thread) is attached, so the
|
||||
// frames compared below are GPU frames, not ones the CPU composited meanwhile (see ui_gpu.rs).
|
||||
wait_for_compositor(&mut h);
|
||||
if gl && h.state().gpu_adapter().is_none() {
|
||||
eprintln!("egui-wgpu's GL device has WebGL2 limits: the viewer stays on the CPU, skipping");
|
||||
return;
|
||||
}
|
||||
assert!(h.state().gpu_adapter().is_some(), "GPU compositor on egui-wgpu's device");
|
||||
let s = &mut h.state_mut().session;
|
||||
s.execute("file.openDemoProject", json!({})).unwrap();
|
||||
s.execute("comp.open", json!({"comp": "3D Showcase"})).unwrap();
|
||||
s.execute("time.set", json!({"time": 5.0})).unwrap();
|
||||
s.execute("comp.settings", json!({"renderer": "classic3D"})).unwrap();
|
||||
settle(&mut h);
|
||||
let before = h.state_mut().viewer_pixels().expect("GPU frame before adding the light").pixels.clone();
|
||||
|
||||
h.state_mut().session.execute("layer.newLight", json!({"kind": "Environment", "color": [1.0, 0.0, 0.0], "intensity": 100.0})).unwrap();
|
||||
|
||||
@@ -6,10 +6,11 @@ use effectcraft_ui_egui::EffectcraftApp;
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
|
||||
/// Step until the GPU compositor's worker is done and a frame has landed in the viewer.
|
||||
fn settle(h: &mut Harness<'_, EffectcraftApp>) {
|
||||
for _ in 0..600 {
|
||||
for _ in 0..3000 {
|
||||
h.step();
|
||||
if h.state().frames.inflight() == 0 && h.state().frames.last_ms.lock().map(|v| *v > 0.0).unwrap_or(false) {
|
||||
if !h.state().gpu_initializing() && h.state().frames.inflight() == 0 && h.state().frames.last_ms.lock().map(|v| *v > 0.0).unwrap_or(false) {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
@@ -19,6 +20,117 @@ fn settle(h: &mut Harness<'_, EffectcraftApp>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Step until the GPU compositor's worker has handed its result over (attached, or why not).
|
||||
/// The first step starts the build.
|
||||
pub(crate) fn wait_for_compositor(h: &mut Harness<'_, EffectcraftApp>) {
|
||||
h.step();
|
||||
for _ in 0..3000 {
|
||||
if !h.state().gpu_initializing() {
|
||||
return;
|
||||
}
|
||||
h.step();
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
panic!("the GPU compositor's worker gave no result within a minute");
|
||||
}
|
||||
|
||||
/// Whether wgpu finds any adapter at all (egui_kittest's wgpu harness needs one; CI runners
|
||||
/// without a GPU skip).
|
||||
fn any_adapter() -> bool {
|
||||
let instance = eframe::wgpu::Instance::new(eframe::wgpu::InstanceDescriptor::new_without_display_handle());
|
||||
!pollster::block_on(instance.enumerate_adapters(eframe::wgpu::Backends::all())).is_empty()
|
||||
}
|
||||
|
||||
fn small_session() -> Session {
|
||||
let mut s = Session::default();
|
||||
let comp = s.execute("comp.new", json!({"name": "First", "width": 64, "height": 64, "duration": 1})).unwrap()["comp"].as_u64().unwrap();
|
||||
s.execute("layer.newSolid", json!({"comp": comp, "color": "#ff0000"})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
/// The window opens before the GPU compositor exists: its pipelines compile on a worker thread
|
||||
/// (tens of seconds on a cold shader cache), and until then the interface draws, says so, and
|
||||
/// composites on the CPU. The worker is held on a gate so the "preparing" state is observable.
|
||||
#[test]
|
||||
fn the_interface_draws_and_composites_on_the_cpu_while_the_gpu_compositor_is_built() {
|
||||
if !any_adapter() {
|
||||
eprintln!("no wgpu adapter: skipping");
|
||||
return;
|
||||
}
|
||||
let (release, gate) = std::sync::mpsc::channel::<()>();
|
||||
let s = small_session();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).wgpu().build_eframe(move |_| {
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
// The status line's 5 s expiry reads the app clock: a manual one makes the steps below
|
||||
// stand for a known span of time.
|
||||
app.use_manual_clock();
|
||||
app.hooks.before_gpu_build = Some(Box::new(move || {
|
||||
let _ = gate.recv();
|
||||
}));
|
||||
app
|
||||
});
|
||||
// The first frames paint, and a CPU frame reaches the viewer, while the worker still waits.
|
||||
for _ in 0..600 {
|
||||
h.step();
|
||||
if h.state().is_ready() && !h.output().shapes.is_empty() && h.state().frames.last_ms.lock().map(|v| *v > 0.0).unwrap_or(false) {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
assert!(h.state().gpu_initializing(), "the compositor is still being built");
|
||||
assert!(!h.output().shapes.is_empty(), "the interface draws before the compositor exists");
|
||||
assert!(h.state().session.accel.is_none(), "nothing renders on a compositor that doesn't exist yet");
|
||||
assert_eq!(h.state().ui.status, effectcraft_ui_egui::GPU_PREPARING, "the status line is honest about it");
|
||||
assert_eq!(h.state().session.accel_note.as_deref(), Some(effectcraft_ui_egui::GPU_PREPARING));
|
||||
assert!(h.state().frames.last_ms.lock().map(|v| *v > 0.0).unwrap_or(false), "a frame was composited on the CPU meanwhile");
|
||||
assert!(h.state_mut().viewer_pixels().is_some(), "the viewer shows that frame");
|
||||
// Other statuses clear after 5 s; this one stays for as long as the build takes (each step
|
||||
// is a quarter of a second).
|
||||
for _ in 0..30 {
|
||||
h.state_mut().clock_mut().advance(0.25);
|
||||
h.step();
|
||||
}
|
||||
assert!(h.state().gpu_initializing());
|
||||
assert_eq!(h.state().ui.status, effectcraft_ui_egui::GPU_PREPARING, "the status line still says so 7.5 s later");
|
||||
release.send(()).unwrap();
|
||||
settle(&mut h);
|
||||
assert!(!h.state().gpu_initializing());
|
||||
assert_ne!(h.state().ui.status, effectcraft_ui_egui::GPU_PREPARING, "the preparing status goes away");
|
||||
match h.state().gpu_adapter() {
|
||||
Some(adapter) => {
|
||||
eprintln!("compositor attached after the first frames: {adapter}");
|
||||
assert!(h.state().session.accel.is_some());
|
||||
assert!(h.state().session.accel_note.is_none());
|
||||
}
|
||||
// An adapter that can't run the compositor (software GL): the reason is recorded.
|
||||
None => assert!(h.state().session.accel_note.as_deref().is_some_and(|n| n != effectcraft_ui_egui::GPU_PREPARING)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A graphics backend that panics while the compositor's pipelines compile panics on the worker
|
||||
/// thread, not under the UI: the window keeps drawing on the CPU and says why.
|
||||
#[test]
|
||||
fn a_compositor_build_that_panics_leaves_the_viewer_on_the_cpu() {
|
||||
if !any_adapter() {
|
||||
eprintln!("no wgpu adapter: skipping");
|
||||
return;
|
||||
}
|
||||
let s = small_session();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).wgpu().build_eframe(move |_| {
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
app.hooks.before_gpu_build = Some(Box::new(|| panic!("synthetic shader compiler panic")));
|
||||
app
|
||||
});
|
||||
settle(&mut h);
|
||||
assert!(!h.state().gpu_initializing());
|
||||
assert!(h.state().gpu_adapter().is_none());
|
||||
assert!(h.state().session.accel.is_none());
|
||||
let note = h.state().session.accel_note.clone().unwrap_or_default();
|
||||
assert!(note.contains("synthetic shader compiler panic"), "{note}");
|
||||
assert!(!h.output().shapes.is_empty(), "the interface still draws");
|
||||
assert!(h.state_mut().viewer_pixels().is_some(), "the viewer shows a CPU frame");
|
||||
}
|
||||
|
||||
/// egui_kittest's wgpu setup on the adapter the headless compositor picked (`name (Backend)`),
|
||||
/// like the desktop app's window. Left alone, egui_kittest prefers a CPU adapter, which on
|
||||
/// Windows is WARP on DirectX 12, where FXC fails to compile the compositor's kernels: the
|
||||
@@ -50,21 +162,24 @@ fn viewer_draws_gpu_frames_that_match_the_cpu() {
|
||||
// egui-wgpu creates its GL device with WebGL2's limits, which the compositor declines.
|
||||
let gl = adapter.ends_with("(Gl)");
|
||||
drop(probe);
|
||||
let mut s = Session::default();
|
||||
s.execute("file.openDemoProject", json!({})).unwrap();
|
||||
s.execute("time.set", json!({"time": 2.0})).unwrap();
|
||||
let mut h = Harness::builder()
|
||||
.with_size(egui::vec2(1400.0, 900.0))
|
||||
.with_pixels_per_point(1.0)
|
||||
.wgpu_setup(same_adapter(adapter))
|
||||
.build_eframe(|_| EffectcraftApp::new(s));
|
||||
settle(&mut h);
|
||||
.build_eframe(|_| EffectcraftApp::new(Session::default()));
|
||||
// The project opens once the compositor (built on a worker thread) is attached. A frame the
|
||||
// CPU composited while it was being built has the GPU frame's key, so the viewer would keep
|
||||
// showing it, and the comparison below would be the CPU against itself.
|
||||
wait_for_compositor(&mut h);
|
||||
if gl && h.state().gpu_adapter().is_none() {
|
||||
eprintln!("egui-wgpu's GL device has WebGL2 limits: the viewer stays on the CPU, skipping");
|
||||
return;
|
||||
}
|
||||
assert!(h.state().gpu_adapter().is_some(), "GPU compositor on egui-wgpu's device");
|
||||
eprintln!("viewer compositor adapter: {:?}", h.state().gpu_adapter());
|
||||
h.state_mut().session.execute("file.openDemoProject", json!({})).unwrap();
|
||||
h.state_mut().session.execute("time.set", json!({"time": 2.0})).unwrap();
|
||||
settle(&mut h);
|
||||
let gpu_px = h.state_mut().viewer_pixels().expect("read back the GPU frame");
|
||||
let gpu_shot = h.render().expect("render");
|
||||
// Mercury Software Only: the same frame from the CPU.
|
||||
|
||||
Reference in New Issue
Block a user