Support Vello CPU fallback on systems with older GPUs

This commit is contained in:
Skyler Lehmkuhl 2026-03-10 21:39:01 -04:00
parent 7a3f522735
commit ce7ed2586f
6 changed files with 68 additions and 28 deletions

View File

@ -37,15 +37,13 @@ pub struct BackendContext<'a> {
/// Audio engine controller (optional - may not be initialized) /// Audio engine controller (optional - may not be initialized)
pub audio_controller: Option<&'a mut daw_backend::EngineController>, pub audio_controller: Option<&'a mut daw_backend::EngineController>,
/// Mapping from document layer UUIDs to backend track IDs /// Mapping from all document layer/clip/group UUIDs to backend track IDs.
/// Covers audio layers, MIDI layers, group layers, and vector clip metatracks.
pub layer_to_track_map: &'a HashMap<Uuid, daw_backend::TrackId>, pub layer_to_track_map: &'a HashMap<Uuid, daw_backend::TrackId>,
/// Mapping from document clip instance UUIDs to backend clip instance IDs /// Mapping from document clip instance UUIDs to backend clip instance IDs
pub clip_instance_to_backend_map: &'a mut HashMap<Uuid, BackendClipInstanceId>, pub clip_instance_to_backend_map: &'a mut HashMap<Uuid, BackendClipInstanceId>,
/// Mapping from movie clip UUIDs to backend metatrack (group track) TrackIds
pub clip_to_metatrack_map: &'a HashMap<Uuid, daw_backend::TrackId>,
// Future: pub video_controller: Option<&'a mut VideoController>, // Future: pub video_controller: Option<&'a mut VideoController>,
} }

View File

@ -203,7 +203,7 @@ impl Action for MoveClipInstancesAction {
for (instance_id, _old_start, new_start) in moves { for (instance_id, _old_start, new_start) in moves {
if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) { if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) {
// Check if this clip has a metatrack // Check if this clip has a metatrack
if let Some(&metatrack_id) = backend.clip_to_metatrack_map.get(&instance.clip_id) { if let Some(&metatrack_id) = backend.layer_to_track_map.get(&instance.clip_id) {
controller.set_offset(metatrack_id, *new_start); controller.set_offset(metatrack_id, *new_start);
controller.set_trim_start(metatrack_id, instance.trim_start); controller.set_trim_start(metatrack_id, instance.trim_start);
controller.set_trim_end(metatrack_id, instance.trim_end); controller.set_trim_end(metatrack_id, instance.trim_end);
@ -287,7 +287,7 @@ impl Action for MoveClipInstancesAction {
if let AnyLayer::Vector(vl) = layer { if let AnyLayer::Vector(vl) = layer {
for (instance_id, old_start, _new_start) in moves { for (instance_id, old_start, _new_start) in moves {
if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) { if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) {
if let Some(&metatrack_id) = backend.clip_to_metatrack_map.get(&instance.clip_id) { if let Some(&metatrack_id) = backend.layer_to_track_map.get(&instance.clip_id) {
controller.set_offset(metatrack_id, *old_start); controller.set_offset(metatrack_id, *old_start);
controller.set_trim_start(metatrack_id, instance.trim_start); controller.set_trim_start(metatrack_id, instance.trim_start);
controller.set_trim_end(metatrack_id, instance.trim_end); controller.set_trim_end(metatrack_id, instance.trim_end);

View File

@ -369,7 +369,7 @@ impl Action for TrimClipInstancesAction {
if let AnyLayer::Vector(vl) = layer { if let AnyLayer::Vector(vl) = layer {
for (instance_id, _trim_type, _old, _new) in trims { for (instance_id, _trim_type, _old, _new) in trims {
if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) { if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) {
if let Some(&metatrack_id) = backend.clip_to_metatrack_map.get(&instance.clip_id) { if let Some(&metatrack_id) = backend.layer_to_track_map.get(&instance.clip_id) {
// Instance already has new values after execute() // Instance already has new values after execute()
controller.set_offset(metatrack_id, instance.timeline_start); controller.set_offset(metatrack_id, instance.timeline_start);
controller.set_trim_start(metatrack_id, instance.trim_start); controller.set_trim_start(metatrack_id, instance.trim_start);
@ -459,7 +459,7 @@ impl Action for TrimClipInstancesAction {
if let AnyLayer::Vector(vl) = layer { if let AnyLayer::Vector(vl) = layer {
for (instance_id, _trim_type, _old, _new) in trims { for (instance_id, _trim_type, _old, _new) in trims {
if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) { if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) {
if let Some(&metatrack_id) = backend.clip_to_metatrack_map.get(&instance.clip_id) { if let Some(&metatrack_id) = backend.layer_to_track_map.get(&instance.clip_id) {
// Instance already has old values after rollback() // Instance already has old values after rollback()
controller.set_offset(metatrack_id, instance.timeline_start); controller.set_offset(metatrack_id, instance.timeline_start);
controller.set_trim_start(metatrack_id, instance.trim_start); controller.set_trim_start(metatrack_id, instance.trim_start);

View File

@ -61,9 +61,6 @@ pub struct SerializedAudioBackend {
#[serde(default)] #[serde(default)]
pub layer_to_track_map: std::collections::HashMap<uuid::Uuid, u32>, pub layer_to_track_map: std::collections::HashMap<uuid::Uuid, u32>,
/// Mapping from movie clip UUIDs to backend metatrack (group track) TrackIds
#[serde(default)]
pub clip_to_metatrack_map: std::collections::HashMap<uuid::Uuid, u32>,
} }
/// Settings for saving a project /// Settings for saving a project
@ -100,9 +97,6 @@ pub struct LoadedProject {
/// Mapping from UI layer UUIDs to backend TrackIds (empty for old files) /// Mapping from UI layer UUIDs to backend TrackIds (empty for old files)
pub layer_to_track_map: std::collections::HashMap<uuid::Uuid, u32>, pub layer_to_track_map: std::collections::HashMap<uuid::Uuid, u32>,
/// Mapping from movie clip UUIDs to backend metatrack TrackIds (empty for old files)
pub clip_to_metatrack_map: std::collections::HashMap<uuid::Uuid, u32>,
/// Loaded audio pool entries /// Loaded audio pool entries
pub audio_pool_entries: Vec<AudioPoolEntry>, pub audio_pool_entries: Vec<AudioPoolEntry>,
@ -154,7 +148,6 @@ pub fn save_beam(
audio_project: &mut AudioProject, audio_project: &mut AudioProject,
audio_pool_entries: Vec<AudioPoolEntry>, audio_pool_entries: Vec<AudioPoolEntry>,
layer_to_track_map: &std::collections::HashMap<uuid::Uuid, u32>, layer_to_track_map: &std::collections::HashMap<uuid::Uuid, u32>,
clip_to_metatrack_map: &std::collections::HashMap<uuid::Uuid, u32>,
_settings: &SaveSettings, _settings: &SaveSettings,
) -> Result<(), String> { ) -> Result<(), String> {
let fn_start = std::time::Instant::now(); let fn_start = std::time::Instant::now();
@ -414,7 +407,6 @@ pub fn save_beam(
project: audio_project.clone(), project: audio_project.clone(),
audio_pool_entries: modified_entries, audio_pool_entries: modified_entries,
layer_to_track_map: layer_to_track_map.clone(), layer_to_track_map: layer_to_track_map.clone(),
clip_to_metatrack_map: clip_to_metatrack_map.clone(),
}, },
}; };
eprintln!("📊 [SAVE_BEAM] Step 5: Build BeamProject structure took {:.2}ms", step5_start.elapsed().as_secs_f64() * 1000.0); eprintln!("📊 [SAVE_BEAM] Step 5: Build BeamProject structure took {:.2}ms", step5_start.elapsed().as_secs_f64() * 1000.0);
@ -502,7 +494,6 @@ pub fn load_beam(path: &Path) -> Result<LoadedProject, String> {
let mut audio_project = beam_project.audio_backend.project; let mut audio_project = beam_project.audio_backend.project;
let audio_pool_entries = beam_project.audio_backend.audio_pool_entries; let audio_pool_entries = beam_project.audio_backend.audio_pool_entries;
let layer_to_track_map = beam_project.audio_backend.layer_to_track_map; let layer_to_track_map = beam_project.audio_backend.layer_to_track_map;
let clip_to_metatrack_map = beam_project.audio_backend.clip_to_metatrack_map;
eprintln!("📊 [LOAD_BEAM] Step 5: Extract document and audio state took {:.2}ms", step5_start.elapsed().as_secs_f64() * 1000.0); eprintln!("📊 [LOAD_BEAM] Step 5: Extract document and audio state took {:.2}ms", step5_start.elapsed().as_secs_f64() * 1000.0);
// 6. Rebuild AudioGraphs from presets // 6. Rebuild AudioGraphs from presets
@ -679,7 +670,6 @@ pub fn load_beam(path: &Path) -> Result<LoadedProject, String> {
document, document,
audio_project, audio_project,
layer_to_track_map, layer_to_track_map,
clip_to_metatrack_map,
audio_pool_entries: restored_entries, audio_pool_entries: restored_entries,
missing_files, missing_files,
}) })

View File

@ -68,6 +68,11 @@ struct Args {
/// Use dark theme /// Use dark theme
#[arg(long, conflicts_with = "light")] #[arg(long, conflicts_with = "light")]
dark: bool, dark: bool,
/// Force Vello to use its CPU renderer instead of the GPU.
/// Useful for testing the CPU fallback path or working around GPU driver issues.
#[arg(long)]
cpu_renderer: bool,
} }
fn main() -> eframe::Result { fn main() -> eframe::Result {
@ -89,6 +94,11 @@ fn main() -> eframe::Result {
// Parse command line arguments // Parse command line arguments
let args = Args::parse(); let args = Args::parse();
if args.cpu_renderer {
panes::stage::FORCE_CPU_RENDERER.store(true, std::sync::atomic::Ordering::Relaxed);
println!("⚠️ CPU renderer forced via --cpu-renderer");
}
// Load config to get theme preference // Load config to get theme preference
let config = AppConfig::load(); let config = AppConfig::load();

View File

@ -11,6 +11,11 @@ use lightningbeam_core::layer::{AnyLayer, AudioLayer};
use lightningbeam_core::renderer::RenderedLayerType; use lightningbeam_core::renderer::RenderedLayerType;
use super::{DragClipType, NodePath, PaneRenderer, SharedPaneState}; use super::{DragClipType, NodePath, PaneRenderer, SharedPaneState};
use std::sync::{Arc, Mutex, OnceLock}; use std::sync::{Arc, Mutex, OnceLock};
use std::sync::atomic::{AtomicBool, Ordering};
/// When set to `true` (via `--cpu-renderer`), forces Vello to use its CPU
/// rendering path regardless of GPU capability.
pub static FORCE_CPU_RENDERER: AtomicBool = AtomicBool::new(false);
/// Enable HDR compositing pipeline (per-layer rendering with proper opacity) /// Enable HDR compositing pipeline (per-layer rendering with proper opacity)
/// Set to true to use the new pipeline, false for legacy single-scene rendering /// Set to true to use the new pipeline, false for legacy single-scene rendering
@ -63,15 +68,51 @@ pub struct VelloResourcesMap {
impl SharedVelloResources { impl SharedVelloResources {
pub fn new(device: &wgpu::Device, video_manager: std::sync::Arc<std::sync::Mutex<lightningbeam_core::video::VideoManager>>, target_format: wgpu::TextureFormat) -> Result<Self, String> { pub fn new(device: &wgpu::Device, video_manager: std::sync::Arc<std::sync::Mutex<lightningbeam_core::video::VideoManager>>, target_format: wgpu::TextureFormat) -> Result<Self, String> {
let renderer = vello::Renderer::new( let use_cpu = FORCE_CPU_RENDERER.load(Ordering::Relaxed);
device,
vello::RendererOptions { // wgpu panics (rather than returning Err) when shader validation fails, so we
use_cpu: false, // catch panics here and fall back to Vello's CPU renderer. This commonly
antialiasing_support: vello::AaSupport::all(), // happens on old GPUs lacking SHADER_FLOAT16_IN_FLOAT32 (required by Vello's
num_init_threads: std::num::NonZeroUsize::new(1), // flatten shader via unpack2x16float). The CPU path uses pre-compiled Rust
pipeline_cache: None, // implementations of the same compute shaders, so no GPU shader compilation
}, // occurs and the capability check is bypassed entirely.
).map_err(|e| format!("Failed to create Vello renderer: {}", e))?; let gpu_result = if use_cpu {
// Skip GPU attempt entirely when forced via --cpu-renderer.
Err(Box::new("cpu-renderer flag set") as Box<dyn std::any::Any + Send>)
} else {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
vello::Renderer::new(
device,
vello::RendererOptions {
use_cpu: false,
antialiasing_support: vello::AaSupport::all(),
num_init_threads: std::num::NonZeroUsize::new(1),
pipeline_cache: None,
},
)
}))
};
let renderer = match gpu_result {
Ok(Ok(r)) => r,
Ok(Err(e)) => return Err(format!("Failed to create Vello renderer: {e}")),
Err(_) => {
if !use_cpu {
eprintln!(
"WARNING: GPU Vello renderer failed to initialise (missing shader \
capability). Falling back to CPU renderer performance may be reduced."
);
}
vello::Renderer::new(
device,
vello::RendererOptions {
use_cpu: true,
antialiasing_support: vello::AaSupport::all(),
num_init_threads: std::num::NonZeroUsize::new(1),
pipeline_cache: None,
},
).map_err(|e| format!("CPU fallback renderer also failed: {e}"))?
}
};
// Create blit shader for rendering texture to screen // Create blit shader for rendering texture to screen
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
@ -480,7 +521,8 @@ impl egui_wgpu::CallbackTrait for VelloCallback {
// Initialize shared resources if not yet created (only happens once for first Stage pane) // Initialize shared resources if not yet created (only happens once for first Stage pane)
if map.shared.is_none() { if map.shared.is_none() {
map.shared = Some(Arc::new( map.shared = Some(Arc::new(
SharedVelloResources::new(device, self.ctx.video_manager.clone(), self.ctx.target_format).expect("Failed to initialize shared Vello resources") SharedVelloResources::new(device, self.ctx.video_manager.clone(), self.ctx.target_format)
.unwrap_or_else(|e| panic!("{}", e))
)); ));
} }