diff --git a/lightningbeam-ui/lightningbeam-core/src/action.rs b/lightningbeam-ui/lightningbeam-core/src/action.rs index a0c4321..d09613f 100644 --- a/lightningbeam-ui/lightningbeam-core/src/action.rs +++ b/lightningbeam-ui/lightningbeam-core/src/action.rs @@ -37,15 +37,13 @@ pub struct BackendContext<'a> { /// Audio engine controller (optional - may not be initialized) 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, /// Mapping from document clip instance UUIDs to backend clip instance IDs pub clip_instance_to_backend_map: &'a mut HashMap, - /// Mapping from movie clip UUIDs to backend metatrack (group track) TrackIds - pub clip_to_metatrack_map: &'a HashMap, - // Future: pub video_controller: Option<&'a mut VideoController>, } diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/move_clip_instances.rs b/lightningbeam-ui/lightningbeam-core/src/actions/move_clip_instances.rs index b14be04..fba7ee7 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/move_clip_instances.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/move_clip_instances.rs @@ -203,7 +203,7 @@ impl Action for MoveClipInstancesAction { for (instance_id, _old_start, new_start) in moves { if let Some(instance) = vl.clip_instances.iter().find(|ci| ci.id == *instance_id) { // 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_trim_start(metatrack_id, instance.trim_start); controller.set_trim_end(metatrack_id, instance.trim_end); @@ -287,7 +287,7 @@ impl Action for MoveClipInstancesAction { if let AnyLayer::Vector(vl) = layer { 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(&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_trim_start(metatrack_id, instance.trim_start); controller.set_trim_end(metatrack_id, instance.trim_end); diff --git a/lightningbeam-ui/lightningbeam-core/src/actions/trim_clip_instances.rs b/lightningbeam-ui/lightningbeam-core/src/actions/trim_clip_instances.rs index efa828b..080b278 100644 --- a/lightningbeam-ui/lightningbeam-core/src/actions/trim_clip_instances.rs +++ b/lightningbeam-ui/lightningbeam-core/src/actions/trim_clip_instances.rs @@ -369,7 +369,7 @@ impl Action for TrimClipInstancesAction { if let AnyLayer::Vector(vl) = layer { 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(&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() controller.set_offset(metatrack_id, instance.timeline_start); controller.set_trim_start(metatrack_id, instance.trim_start); @@ -459,7 +459,7 @@ impl Action for TrimClipInstancesAction { if let AnyLayer::Vector(vl) = layer { 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(&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() controller.set_offset(metatrack_id, instance.timeline_start); controller.set_trim_start(metatrack_id, instance.trim_start); diff --git a/lightningbeam-ui/lightningbeam-core/src/file_io.rs b/lightningbeam-ui/lightningbeam-core/src/file_io.rs index e8a501e..9ef8506 100644 --- a/lightningbeam-ui/lightningbeam-core/src/file_io.rs +++ b/lightningbeam-ui/lightningbeam-core/src/file_io.rs @@ -61,9 +61,6 @@ pub struct SerializedAudioBackend { #[serde(default)] pub layer_to_track_map: std::collections::HashMap, - /// Mapping from movie clip UUIDs to backend metatrack (group track) TrackIds - #[serde(default)] - pub clip_to_metatrack_map: std::collections::HashMap, } /// Settings for saving a project @@ -100,9 +97,6 @@ pub struct LoadedProject { /// Mapping from UI layer UUIDs to backend TrackIds (empty for old files) pub layer_to_track_map: std::collections::HashMap, - /// Mapping from movie clip UUIDs to backend metatrack TrackIds (empty for old files) - pub clip_to_metatrack_map: std::collections::HashMap, - /// Loaded audio pool entries pub audio_pool_entries: Vec, @@ -154,7 +148,6 @@ pub fn save_beam( audio_project: &mut AudioProject, audio_pool_entries: Vec, layer_to_track_map: &std::collections::HashMap, - clip_to_metatrack_map: &std::collections::HashMap, _settings: &SaveSettings, ) -> Result<(), String> { let fn_start = std::time::Instant::now(); @@ -414,7 +407,6 @@ pub fn save_beam( project: audio_project.clone(), audio_pool_entries: modified_entries, 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); @@ -502,7 +494,6 @@ pub fn load_beam(path: &Path) -> Result { let mut audio_project = beam_project.audio_backend.project; 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 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); // 6. Rebuild AudioGraphs from presets @@ -679,7 +670,6 @@ pub fn load_beam(path: &Path) -> Result { document, audio_project, layer_to_track_map, - clip_to_metatrack_map, audio_pool_entries: restored_entries, missing_files, }) diff --git a/lightningbeam-ui/lightningbeam-editor/src/main.rs b/lightningbeam-ui/lightningbeam-editor/src/main.rs index a12a776..c34c9d4 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/main.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/main.rs @@ -68,6 +68,11 @@ struct Args { /// Use dark theme #[arg(long, conflicts_with = "light")] 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 { @@ -89,6 +94,11 @@ fn main() -> eframe::Result { // Parse command line arguments 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 let config = AppConfig::load(); diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs index 4a9e487..f91085f 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -11,6 +11,11 @@ use lightningbeam_core::layer::{AnyLayer, AudioLayer}; use lightningbeam_core::renderer::RenderedLayerType; use super::{DragClipType, NodePath, PaneRenderer, SharedPaneState}; 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) /// Set to true to use the new pipeline, false for legacy single-scene rendering @@ -63,15 +68,51 @@ pub struct VelloResourcesMap { impl SharedVelloResources { pub fn new(device: &wgpu::Device, video_manager: std::sync::Arc>, target_format: wgpu::TextureFormat) -> Result { - let renderer = 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, - }, - ).map_err(|e| format!("Failed to create Vello renderer: {}", e))?; + let use_cpu = FORCE_CPU_RENDERER.load(Ordering::Relaxed); + + // wgpu panics (rather than returning Err) when shader validation fails, so we + // catch panics here and fall back to Vello's CPU renderer. This commonly + // happens on old GPUs lacking SHADER_FLOAT16_IN_FLOAT32 (required by Vello's + // flatten shader via unpack2x16float). The CPU path uses pre-compiled Rust + // implementations of the same compute shaders, so no GPU shader compilation + // occurs and the capability check is bypassed entirely. + let gpu_result = if use_cpu { + // Skip GPU attempt entirely when forced via --cpu-renderer. + Err(Box::new("cpu-renderer flag set") as Box) + } 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 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) if map.shared.is_none() { 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)) )); }