Skip to main content

ringkernel_wavesim/gui/
app.rs

1//! Main Iced Application for the wave simulation.
2
3use super::canvas::GridCanvas;
4use super::controls;
5use crate::simulation::{
6    AcousticParams, CellType, EducationalProcessor, KernelGrid, SimulationGrid, SimulationMode,
7};
8
9#[cfg(feature = "cuda")]
10use crate::simulation::CudaPackedBackend;
11
12use iced::widget::{container, row, Canvas};
13use iced::{Element, Length, Size, Subscription, Task, Theme};
14use ringkernel::prelude::Backend;
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17use tokio::sync::Mutex;
18
19/// Available compute backends for the GUI.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ComputeBackend {
22    /// CPU with SoA + SIMD + Rayon optimization.
23    Cpu,
24    /// GPU kernel actor model (WGPU/CUDA per-tile).
25    GpuActor,
26    /// CUDA Packed backend (GPU-only halo exchange).
27    #[cfg(feature = "cuda")]
28    CudaPacked,
29}
30
31impl std::fmt::Display for ComputeBackend {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            ComputeBackend::Cpu => write!(f, "CPU (SIMD+Rayon)"),
35            ComputeBackend::GpuActor => write!(f, "GPU Actor"),
36            #[cfg(feature = "cuda")]
37            ComputeBackend::CudaPacked => write!(f, "CUDA Packed"),
38        }
39    }
40}
41
42/// Drawing mode for cell manipulation.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum DrawMode {
45    /// Normal mode - left click injects impulse.
46    #[default]
47    Impulse,
48    /// Draw absorber cells (absorb waves).
49    Absorber,
50    /// Draw reflector cells (reflect waves like walls).
51    Reflector,
52    /// Erase cell types (reset to normal).
53    Erase,
54}
55
56impl std::fmt::Display for DrawMode {
57    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58        match self {
59            DrawMode::Impulse => write!(f, "Impulse"),
60            DrawMode::Absorber => write!(f, "Absorber"),
61            DrawMode::Reflector => write!(f, "Reflector"),
62            DrawMode::Erase => write!(f, "Erase"),
63        }
64    }
65}
66
67/// Simulation engine abstraction for CPU or GPU modes.
68enum SimulationEngine {
69    /// CPU-based synchronous simulation.
70    Cpu(SimulationGrid),
71    /// GPU-based kernel actor simulation.
72    Gpu(Arc<Mutex<KernelGrid>>),
73    /// CUDA Packed backend (GPU-only halo exchange).
74    #[cfg(feature = "cuda")]
75    CudaPacked(Arc<std::sync::Mutex<CudaPackedBackend>>),
76    /// Transitioning state (while switching backends).
77    Switching,
78}
79
80/// The main wave simulation application.
81pub struct WaveSimApp {
82    /// The simulation engine (CPU or GPU).
83    engine: SimulationEngine,
84    /// The canvas for rendering.
85    canvas: GridCanvas,
86
87    // Cached grid dimensions (for GPU mode)
88    grid_width: u32,
89    grid_height: u32,
90
91    // UI state
92    /// Current grid width setting.
93    grid_width_input: String,
94    /// Current grid height setting.
95    grid_height_input: String,
96    /// Current speed of sound.
97    speed_of_sound: f32,
98    /// Current cell size in meters.
99    cell_size: f32,
100    /// Selected compute backend.
101    compute_backend: ComputeBackend,
102    /// Legacy backend for GPU actor mode.
103    legacy_backend: Backend,
104    /// Whether simulation is running.
105    is_running: bool,
106    /// Whether to show stats panel.
107    show_stats: bool,
108    /// Impulse amplitude for clicks.
109    impulse_amplitude: f32,
110    /// Current drawing mode.
111    draw_mode: DrawMode,
112    /// Cell types for drawing (CPU mode uses grid's internal, CUDA uses this).
113    cell_types: Vec<Vec<CellType>>,
114    /// Educational simulation mode.
115    simulation_mode: SimulationMode,
116    /// Educational processor for visualization modes.
117    educational_processor: EducationalProcessor,
118
119    // Performance tracking
120    /// Last frame time for FPS calculation.
121    last_frame: Instant,
122    /// Current FPS (frames per second).
123    fps: f32,
124    /// Steps per second (simulation steps).
125    steps_per_sec: f32,
126    /// Cell throughput (cells updated per second).
127    throughput: f64,
128    /// Steps executed in the last frame.
129    steps_last_frame: u32,
130    /// Accumulated step count for averaging.
131    step_accumulator: u32,
132    /// Time accumulator for averaging.
133    time_accumulator: Duration,
134    /// Last time stats were updated.
135    last_stats_update: Instant,
136
137    // Stats (updated from grid)
138    cell_count: usize,
139    courant_number: f32,
140    max_pressure: f32,
141    total_energy: f32,
142}
143
144/// Messages for the application.
145#[derive(Debug, Clone)]
146pub enum Message {
147    /// Toggle simulation running state.
148    ToggleRunning,
149    /// Perform a single simulation step.
150    Step,
151    /// Reset the simulation.
152    Reset,
153
154    /// Speed of sound slider changed.
155    SpeedChanged(f32),
156    /// Grid width input changed.
157    GridWidthChanged(String),
158    /// Grid height input changed.
159    GridHeightChanged(String),
160    /// Apply grid size changes.
161    ApplyGridSize,
162
163    /// Impulse amplitude changed.
164    ImpulseAmplitudeChanged(f32),
165
166    /// Cell size changed.
167    CellSizeChanged(f32),
168
169    /// Compute backend changed.
170    ComputeBackendChanged(ComputeBackend),
171    /// Backend switch completed (for GPU actor mode).
172    BackendSwitched(Result<Arc<Mutex<KernelGrid>>, String>),
173    /// CUDA Packed backend switch completed.
174    #[cfg(feature = "cuda")]
175    CudaPackedSwitched(Result<Arc<std::sync::Mutex<CudaPackedBackend>>, String>),
176
177    /// User clicked on the canvas (left click).
178    CanvasClick(f32, f32),
179    /// User right-clicked on the canvas (for drawing).
180    CanvasRightClick(f32, f32),
181    /// Drawing mode changed.
182    DrawModeChanged(DrawMode),
183    /// Clear all drawn cell types.
184    ClearCellTypes,
185
186    /// Animation tick.
187    Tick,
188    /// GPU step completed (for actor mode).
189    GpuStepCompleted(Vec<Vec<f32>>, f32, f32),
190    /// CUDA Packed step completed.
191    #[cfg(feature = "cuda")]
192    CudaPackedStepCompleted(Vec<f32>, u32),
193
194    /// Toggle stats display.
195    ToggleStats,
196    /// Simulation mode changed.
197    SimulationModeChanged(SimulationMode),
198}
199
200impl WaveSimApp {
201    /// Create a new application instance.
202    pub fn new() -> (Self, Task<Message>) {
203        let params = AcousticParams::new(343.0, 1.0);
204        let grid = SimulationGrid::new(64, 64, params.clone());
205        let pressure_grid = grid.get_pressure_grid();
206
207        let cell_count = grid.cell_count();
208        let courant_number = grid.params.courant_number();
209        let now = Instant::now();
210
211        (
212            Self {
213                engine: SimulationEngine::Cpu(grid),
214                canvas: GridCanvas::new(pressure_grid),
215                grid_width: 64,
216                grid_height: 64,
217                grid_width_input: "64".to_string(),
218                grid_height_input: "64".to_string(),
219                speed_of_sound: 343.0,
220                cell_size: 1.0,
221                compute_backend: ComputeBackend::Cpu,
222                legacy_backend: Backend::Cpu,
223                is_running: false,
224                show_stats: true,
225                impulse_amplitude: 1.0,
226                draw_mode: DrawMode::Impulse,
227                cell_types: vec![vec![CellType::Normal; 64]; 64],
228                simulation_mode: SimulationMode::Standard,
229                educational_processor: EducationalProcessor::default(),
230                last_frame: now,
231                fps: 0.0,
232                steps_per_sec: 0.0,
233                throughput: 0.0,
234                steps_last_frame: 0,
235                step_accumulator: 0,
236                time_accumulator: Duration::ZERO,
237                last_stats_update: now,
238                cell_count,
239                courant_number,
240                max_pressure: 0.0,
241                total_energy: 0.0,
242            },
243            Task::none(),
244        )
245    }
246
247    /// Application title.
248    pub fn title(&self) -> String {
249        format!(
250            "RingKernel WaveSim - {}x{} Grid ({})",
251            self.grid_width, self.grid_height, self.compute_backend
252        )
253    }
254
255    /// Handle messages.
256    pub fn update(&mut self, message: Message) -> Task<Message> {
257        match message {
258            Message::ToggleRunning => {
259                self.is_running = !self.is_running;
260                // Reset performance stats when starting
261                if self.is_running {
262                    self.step_accumulator = 0;
263                    self.time_accumulator = Duration::ZERO;
264                    self.last_stats_update = Instant::now();
265                }
266            }
267
268            Message::Step => {
269                return self.step_simulation();
270            }
271
272            Message::Reset => {
273                self.reset_performance_stats();
274                match &self.engine {
275                    SimulationEngine::Cpu(_) => {
276                        if let SimulationEngine::Cpu(grid) = &mut self.engine {
277                            grid.reset();
278                            let pressure_grid = grid.get_pressure_grid();
279                            self.max_pressure = grid.max_pressure();
280                            self.total_energy = grid.total_energy();
281                            self.canvas.update_pressure(pressure_grid);
282                        }
283                    }
284                    SimulationEngine::Gpu(kernel_grid) => {
285                        let grid = kernel_grid.clone();
286                        return Task::perform(
287                            async move {
288                                let mut g = grid.lock().await;
289                                g.reset();
290                                (g.get_pressure_grid(), g.max_pressure(), g.total_energy())
291                            },
292                            |(pressure, max_p, energy)| {
293                                Message::GpuStepCompleted(pressure, max_p, energy)
294                            },
295                        );
296                    }
297                    #[cfg(feature = "cuda")]
298                    SimulationEngine::CudaPacked(_) => {
299                        // For CUDA Packed, we need to recreate the backend
300                        return self.switch_to_cuda_packed();
301                    }
302                    SimulationEngine::Switching => {}
303                }
304            }
305
306            Message::SpeedChanged(speed) => {
307                self.speed_of_sound = speed;
308                let params = AcousticParams::new(speed, self.cell_size);
309                #[allow(unused_variables)]
310                let c2 = params.courant_number().powi(2);
311                #[allow(unused_variables)]
312                let damping = 1.0 - params.damping;
313                self.courant_number = params.courant_number();
314
315                match &mut self.engine {
316                    SimulationEngine::Cpu(grid) => {
317                        grid.set_speed_of_sound(speed);
318                    }
319                    SimulationEngine::Gpu(kernel_grid) => {
320                        let grid = kernel_grid.clone();
321                        tokio::spawn(async move {
322                            grid.lock().await.set_speed_of_sound(speed);
323                        });
324                    }
325                    #[cfg(feature = "cuda")]
326                    SimulationEngine::CudaPacked(backend) => {
327                        if let Ok(mut b) = backend.lock() {
328                            b.set_params(c2, damping);
329                        }
330                    }
331                    SimulationEngine::Switching => {}
332                }
333            }
334
335            Message::GridWidthChanged(s) => {
336                self.grid_width_input = s;
337            }
338
339            Message::GridHeightChanged(s) => {
340                self.grid_height_input = s;
341            }
342
343            Message::ApplyGridSize => {
344                if let (Ok(w), Ok(h)) = (
345                    self.grid_width_input.parse::<u32>(),
346                    self.grid_height_input.parse::<u32>(),
347                ) {
348                    // Allow larger grids for CUDA Packed
349                    #[cfg(feature = "cuda")]
350                    let max_size = if matches!(self.compute_backend, ComputeBackend::CudaPacked) {
351                        512
352                    } else {
353                        256
354                    };
355                    #[cfg(not(feature = "cuda"))]
356                    let max_size = 256;
357
358                    let w = w.clamp(16, max_size);
359                    let h = h.clamp(16, max_size);
360                    self.grid_width = w;
361                    self.grid_height = h;
362                    self.grid_width_input = w.to_string();
363                    self.grid_height_input = h.to_string();
364                    self.cell_count = (w * h) as usize;
365                    self.reset_performance_stats();
366
367                    // Recreate the engine with new size
368                    return self.switch_backend(self.compute_backend);
369                }
370            }
371
372            Message::ImpulseAmplitudeChanged(amp) => {
373                self.impulse_amplitude = amp;
374            }
375
376            Message::CellSizeChanged(size) => {
377                self.cell_size = size;
378                let params = AcousticParams::new(self.speed_of_sound, size);
379                #[allow(unused_variables)]
380                let c2 = params.courant_number().powi(2);
381                #[allow(unused_variables)]
382                let damping = 1.0 - params.damping;
383                self.courant_number = params.courant_number();
384
385                match &mut self.engine {
386                    SimulationEngine::Cpu(grid) => {
387                        grid.set_cell_size(size);
388                    }
389                    SimulationEngine::Gpu(kernel_grid) => {
390                        let grid = kernel_grid.clone();
391                        tokio::spawn(async move {
392                            grid.lock().await.set_cell_size(size);
393                        });
394                    }
395                    #[cfg(feature = "cuda")]
396                    SimulationEngine::CudaPacked(backend) => {
397                        if let Ok(mut b) = backend.lock() {
398                            b.set_params(c2, damping);
399                        }
400                    }
401                    SimulationEngine::Switching => {}
402                }
403            }
404
405            Message::ComputeBackendChanged(new_backend) => {
406                if new_backend == self.compute_backend {
407                    return Task::none();
408                }
409                tracing::info!(
410                    "Switching compute backend from {} to {}",
411                    self.compute_backend,
412                    new_backend
413                );
414                self.compute_backend = new_backend;
415                self.reset_performance_stats();
416                return self.switch_backend(new_backend);
417            }
418
419            Message::BackendSwitched(result) => match result {
420                Ok(kernel_grid) => {
421                    self.engine = SimulationEngine::Gpu(kernel_grid.clone());
422                    return Task::perform(
423                        async move {
424                            let g = kernel_grid.lock().await;
425                            (g.get_pressure_grid(), g.max_pressure(), g.total_energy())
426                        },
427                        |(pressure, max_p, energy)| {
428                            Message::GpuStepCompleted(pressure, max_p, energy)
429                        },
430                    );
431                }
432                Err(e) => {
433                    tracing::error!("Failed to switch backend: {}", e);
434                    self.compute_backend = ComputeBackend::Cpu;
435                    let params = AcousticParams::new(self.speed_of_sound, self.cell_size);
436                    let grid = SimulationGrid::new(self.grid_width, self.grid_height, params);
437                    self.cell_count = grid.cell_count();
438                    self.courant_number = grid.params.courant_number();
439                    let pressure = grid.get_pressure_grid();
440                    self.engine = SimulationEngine::Cpu(grid);
441                    self.canvas.update_pressure(pressure);
442                }
443            },
444
445            #[cfg(feature = "cuda")]
446            Message::CudaPackedSwitched(result) => {
447                match result {
448                    Ok(backend) => {
449                        self.engine = SimulationEngine::CudaPacked(backend.clone());
450                        // Read initial state
451                        let pressure = {
452                            let b = backend.lock().unwrap();
453                            b.read_pressure_grid()
454                                .unwrap_or_else(|_| vec![0.0; self.cell_count])
455                        };
456                        self.update_canvas_from_flat(&pressure);
457                    }
458                    Err(e) => {
459                        tracing::error!("Failed to create CUDA Packed backend: {}", e);
460                        self.compute_backend = ComputeBackend::Cpu;
461                        let params = AcousticParams::new(self.speed_of_sound, self.cell_size);
462                        let grid = SimulationGrid::new(self.grid_width, self.grid_height, params);
463                        self.cell_count = grid.cell_count();
464                        self.courant_number = grid.params.courant_number();
465                        let pressure = grid.get_pressure_grid();
466                        self.engine = SimulationEngine::Cpu(grid);
467                        self.canvas.update_pressure(pressure);
468                    }
469                }
470            }
471
472            Message::CanvasClick(x, y) => {
473                let gx = (x * self.grid_width as f32) as u32;
474                let gy = (y * self.grid_height as f32) as u32;
475                let gx = gx.min(self.grid_width - 1);
476                let gy = gy.min(self.grid_height - 1);
477
478                // Handle based on draw mode
479                match self.draw_mode {
480                    DrawMode::Impulse => {
481                        // Original impulse behavior
482                        match &self.engine {
483                            SimulationEngine::Cpu(_) => {
484                                if let SimulationEngine::Cpu(grid) = &mut self.engine {
485                                    grid.inject_impulse(gx, gy, self.impulse_amplitude);
486                                    let pressure_grid = grid.get_pressure_grid();
487                                    self.max_pressure = grid.max_pressure();
488                                    self.total_energy = grid.total_energy();
489                                    self.canvas.update_pressure(pressure_grid);
490                                }
491                            }
492                            SimulationEngine::Gpu(kernel_grid) => {
493                                let grid = kernel_grid.clone();
494                                let amp = self.impulse_amplitude;
495                                return Task::perform(
496                                    async move {
497                                        let mut g = grid.lock().await;
498                                        g.inject_impulse(gx, gy, amp);
499                                        (g.get_pressure_grid(), g.max_pressure(), g.total_energy())
500                                    },
501                                    |(pressure, max_p, energy)| {
502                                        Message::GpuStepCompleted(pressure, max_p, energy)
503                                    },
504                                );
505                            }
506                            #[cfg(feature = "cuda")]
507                            SimulationEngine::CudaPacked(backend) => {
508                                let backend = backend.clone();
509                                let amp = self.impulse_amplitude;
510                                return Task::perform(
511                                    async move {
512                                        let b = backend.lock().unwrap();
513                                        let _ = b.inject_impulse(gx, gy, amp);
514                                        let pressure = b.read_pressure_grid().unwrap_or_default();
515                                        (pressure, 0u32)
516                                    },
517                                    |(pressure, steps)| {
518                                        Message::CudaPackedStepCompleted(pressure, steps)
519                                    },
520                                );
521                            }
522                            SimulationEngine::Switching => {}
523                        }
524                    }
525                    DrawMode::Absorber | DrawMode::Reflector | DrawMode::Erase => {
526                        // Set cell type
527                        let cell_type = match self.draw_mode {
528                            DrawMode::Absorber => CellType::Absorber,
529                            DrawMode::Reflector => CellType::Reflector,
530                            DrawMode::Erase => CellType::Normal,
531                            _ => CellType::Normal,
532                        };
533                        self.set_cell_type_at(gx, gy, cell_type);
534                    }
535                }
536            }
537
538            Message::CanvasRightClick(x, y) => {
539                // Right-click always sets cell type based on current draw mode
540                let gx = (x * self.grid_width as f32) as u32;
541                let gy = (y * self.grid_height as f32) as u32;
542                let gx = gx.min(self.grid_width - 1);
543                let gy = gy.min(self.grid_height - 1);
544
545                let cell_type = match self.draw_mode {
546                    DrawMode::Impulse => CellType::Normal, // Right-click in impulse mode erases
547                    DrawMode::Absorber => CellType::Absorber,
548                    DrawMode::Reflector => CellType::Reflector,
549                    DrawMode::Erase => CellType::Normal,
550                };
551                self.set_cell_type_at(gx, gy, cell_type);
552            }
553
554            Message::DrawModeChanged(mode) => {
555                self.draw_mode = mode;
556            }
557
558            Message::ClearCellTypes => {
559                // Clear all cell types
560                for row in &mut self.cell_types {
561                    for cell in row {
562                        *cell = CellType::Normal;
563                    }
564                }
565                // Also clear in the CPU grid if applicable
566                if let SimulationEngine::Cpu(grid) = &mut self.engine {
567                    grid.clear_cell_types();
568                }
569                self.canvas.update_cell_types(self.cell_types.clone());
570            }
571
572            Message::Tick => {
573                if self.is_running {
574                    return self.step_simulation();
575                }
576            }
577
578            Message::GpuStepCompleted(pressure_grid, max_pressure, total_energy) => {
579                self.canvas.update_pressure(pressure_grid);
580                self.max_pressure = max_pressure;
581                self.total_energy = total_energy;
582                self.update_frame_stats(self.steps_last_frame);
583            }
584
585            #[cfg(feature = "cuda")]
586            Message::CudaPackedStepCompleted(pressure, steps) => {
587                self.update_canvas_from_flat(&pressure);
588                self.update_frame_stats(steps);
589            }
590
591            Message::ToggleStats => {
592                self.show_stats = !self.show_stats;
593            }
594
595            Message::SimulationModeChanged(mode) => {
596                self.simulation_mode = mode;
597                self.educational_processor.set_mode(mode);
598                tracing::info!("Simulation mode changed to: {}", mode);
599            }
600        }
601
602        Task::none()
603    }
604
605    /// Switch to a new compute backend.
606    fn switch_backend(&mut self, backend: ComputeBackend) -> Task<Message> {
607        match backend {
608            ComputeBackend::Cpu => {
609                let params = AcousticParams::new(self.speed_of_sound, self.cell_size);
610                let grid = SimulationGrid::new(self.grid_width, self.grid_height, params);
611                self.cell_count = grid.cell_count();
612                self.courant_number = grid.params.courant_number();
613                let pressure = grid.get_pressure_grid();
614                self.engine = SimulationEngine::Cpu(grid);
615                self.canvas.update_pressure(pressure);
616                Task::none()
617            }
618            ComputeBackend::GpuActor => {
619                self.engine = SimulationEngine::Switching;
620                let width = self.grid_width;
621                let height = self.grid_height;
622                let speed = self.speed_of_sound;
623                let cell_size = self.cell_size;
624                let legacy_backend = self.legacy_backend;
625
626                Task::perform(
627                    async move {
628                        let params = AcousticParams::new(speed, cell_size);
629                        match KernelGrid::new(width, height, params, legacy_backend).await {
630                            Ok(grid) => Ok(Arc::new(Mutex::new(grid))),
631                            Err(e) => Err(format!("{:?}", e)),
632                        }
633                    },
634                    Message::BackendSwitched,
635                )
636            }
637            #[cfg(feature = "cuda")]
638            ComputeBackend::CudaPacked => self.switch_to_cuda_packed(),
639        }
640    }
641
642    /// Switch to CUDA Packed backend.
643    #[cfg(feature = "cuda")]
644    fn switch_to_cuda_packed(&mut self) -> Task<Message> {
645        self.engine = SimulationEngine::Switching;
646        let width = self.grid_width;
647        let height = self.grid_height;
648        let speed = self.speed_of_sound;
649        let cell_size = self.cell_size;
650
651        Task::perform(
652            async move {
653                let params = AcousticParams::new(speed, cell_size);
654                let c2 = params.courant_number().powi(2);
655                let damping = 1.0 - params.damping;
656
657                match CudaPackedBackend::new(width, height, 16) {
658                    Ok(mut backend) => {
659                        backend.set_params(c2, damping);
660                        Ok(Arc::new(std::sync::Mutex::new(backend)))
661                    }
662                    Err(e) => Err(format!("{:?}", e)),
663                }
664            },
665            Message::CudaPackedSwitched,
666        )
667    }
668
669    /// Reset performance statistics.
670    fn reset_performance_stats(&mut self) {
671        self.fps = 0.0;
672        self.steps_per_sec = 0.0;
673        self.throughput = 0.0;
674        self.steps_last_frame = 0;
675        self.step_accumulator = 0;
676        self.time_accumulator = Duration::ZERO;
677        self.last_stats_update = Instant::now();
678    }
679
680    /// Update frame statistics after a step completes.
681    fn update_frame_stats(&mut self, steps: u32) {
682        let now = Instant::now();
683        let delta = now.duration_since(self.last_frame);
684        self.fps = 1.0 / delta.as_secs_f32();
685        self.last_frame = now;
686        self.steps_last_frame = steps;
687
688        // Accumulate for averaging
689        self.step_accumulator += steps;
690        self.time_accumulator += delta;
691
692        // Update stats every 500ms
693        let stats_delta = now.duration_since(self.last_stats_update);
694        if stats_delta >= Duration::from_millis(500) {
695            let secs = self.time_accumulator.as_secs_f64();
696            if secs > 0.0 {
697                self.steps_per_sec = self.step_accumulator as f32 / secs as f32;
698                self.throughput = (self.step_accumulator as f64 * self.cell_count as f64) / secs;
699            }
700            self.step_accumulator = 0;
701            self.time_accumulator = Duration::ZERO;
702            self.last_stats_update = now;
703        }
704    }
705
706    /// Update canvas from flat pressure array.
707    #[allow(dead_code)]
708    fn update_canvas_from_flat(&mut self, pressure: &[f32]) {
709        let mut grid = Vec::with_capacity(self.grid_height as usize);
710        for y in 0..self.grid_height as usize {
711            let start = y * self.grid_width as usize;
712            let end = start + self.grid_width as usize;
713            if end <= pressure.len() {
714                grid.push(pressure[start..end].to_vec());
715            } else {
716                grid.push(vec![0.0; self.grid_width as usize]);
717            }
718        }
719
720        // Calculate max pressure and energy
721        self.max_pressure = pressure.iter().fold(0.0f32, |acc, &p| acc.max(p.abs()));
722        self.total_energy = pressure.iter().map(|&p| p * p).sum();
723
724        self.canvas.update_pressure(grid);
725    }
726
727    /// Set cell type at the given grid position.
728    fn set_cell_type_at(&mut self, gx: u32, gy: u32, cell_type: CellType) {
729        let gx = gx as usize;
730        let gy = gy as usize;
731
732        // Resize cell_types if needed
733        let height = self.grid_height as usize;
734        let width = self.grid_width as usize;
735        if self.cell_types.len() != height || (height > 0 && self.cell_types[0].len() != width) {
736            self.cell_types = vec![vec![CellType::Normal; width]; height];
737        }
738
739        // Update local cell_types array
740        if gy < self.cell_types.len() && gx < self.cell_types[gy].len() {
741            self.cell_types[gy][gx] = cell_type;
742        }
743
744        // Update simulation grid for CPU mode
745        if let SimulationEngine::Cpu(grid) = &mut self.engine {
746            grid.set_cell_type(gx as u32, gy as u32, cell_type);
747        }
748
749        // Update canvas to show cell types
750        self.canvas.update_cell_types(self.cell_types.clone());
751    }
752
753    /// Perform simulation step(s).
754    fn step_simulation(&mut self) -> Task<Message> {
755        match &self.engine {
756            SimulationEngine::Cpu(_) => {
757                let start = Instant::now();
758                if let SimulationEngine::Cpu(grid) = &mut self.engine {
759                    // Check if we're using educational mode
760                    if self.simulation_mode != SimulationMode::Standard {
761                        // Educational mode: use the educational processor
762                        let (pressure, pressure_prev, width, height, c2, damping) =
763                            grid.get_buffers_mut();
764
765                        let result = self.educational_processor.step_frame(
766                            pressure,
767                            pressure_prev,
768                            width,
769                            height,
770                            c2,
771                            damping,
772                        );
773
774                        if result.step_complete && result.should_swap {
775                            grid.swap_buffers();
776                        }
777
778                        // Update processing indicators on canvas
779                        self.canvas.set_processing_state(
780                            self.educational_processor.state.just_processed.clone(),
781                            self.educational_processor.state.active_tiles.clone(),
782                            self.educational_processor.state.current_row,
783                        );
784
785                        self.steps_last_frame = if result.step_complete { 1 } else { 0 };
786                    } else {
787                        // Standard mode: run multiple steps per frame
788                        let target_frame_time = Duration::from_millis(16);
789                        let dt = grid.params.time_step;
790                        let max_steps = ((0.01 / dt) as u32).clamp(1, 2000);
791
792                        let mut steps = 0u32;
793                        while start.elapsed() < target_frame_time && steps < max_steps {
794                            grid.step();
795                            steps += 1;
796                        }
797
798                        self.steps_last_frame = steps;
799                        // Clear processing indicators
800                        self.canvas.set_processing_state(vec![], vec![], None);
801                    }
802
803                    let pressure_grid = grid.get_pressure_grid();
804                    self.max_pressure = grid.max_pressure();
805                    self.total_energy = grid.total_energy();
806                    self.canvas.update_pressure(pressure_grid);
807                    self.update_frame_stats(self.steps_last_frame);
808                }
809                Task::none()
810            }
811            SimulationEngine::Gpu(kernel_grid) => {
812                let grid = kernel_grid.clone();
813
814                Task::perform(
815                    async move {
816                        let mut g = grid.lock().await;
817                        let dt = g.params.time_step;
818                        let steps = ((0.01 / dt) as u32).clamp(1, 500);
819
820                        for _ in 0..steps {
821                            if let Err(e) = g.step().await {
822                                tracing::error!("GPU step error: {:?}", e);
823                                break;
824                            }
825                        }
826                        (g.get_pressure_grid(), g.max_pressure(), g.total_energy())
827                    },
828                    |(pressure, max_p, energy)| Message::GpuStepCompleted(pressure, max_p, energy),
829                )
830            }
831            #[cfg(feature = "cuda")]
832            SimulationEngine::CudaPacked(backend) => {
833                let backend = backend.clone();
834                let cell_count = self.cell_count;
835
836                Task::perform(
837                    async move {
838                        let mut b = backend.lock().unwrap();
839                        // Run many steps per frame for CUDA Packed (it's fast!)
840                        let steps = 100u32;
841                        if let Err(e) = b.step_batch(steps) {
842                            tracing::error!("CUDA step error: {:?}", e);
843                        }
844                        let pressure = b
845                            .read_pressure_grid()
846                            .unwrap_or_else(|_| vec![0.0; cell_count]);
847                        (pressure, steps)
848                    },
849                    |(pressure, steps)| Message::CudaPackedStepCompleted(pressure, steps),
850                )
851            }
852            SimulationEngine::Switching => Task::none(),
853        }
854    }
855
856    /// Build the view.
857    pub fn view(&self) -> Element<'_, Message> {
858        let canvas = Canvas::new(&self.canvas)
859            .width(Length::Fill)
860            .height(Length::Fill);
861
862        let controls = controls::view_controls(
863            self.is_running,
864            self.speed_of_sound,
865            self.cell_size,
866            &self.grid_width_input,
867            &self.grid_height_input,
868            self.impulse_amplitude,
869            self.compute_backend,
870            self.draw_mode,
871            self.simulation_mode,
872            self.show_stats,
873            self.fps,
874            self.steps_per_sec,
875            self.throughput,
876            self.cell_count,
877            self.courant_number,
878            self.max_pressure,
879            self.total_energy,
880        );
881
882        let content = row![
883            container(canvas)
884                .width(Length::FillPortion(3))
885                .height(Length::Fill)
886                .padding(10),
887            container(controls)
888                .width(Length::FillPortion(1))
889                .height(Length::Fill)
890                .padding(10),
891        ];
892
893        container(content)
894            .width(Length::Fill)
895            .height(Length::Fill)
896            .into()
897    }
898
899    /// Subscriptions for animation.
900    pub fn subscription(&self) -> Subscription<Message> {
901        if self.is_running {
902            iced::time::every(Duration::from_millis(16)).map(|_| Message::Tick)
903        } else {
904            Subscription::none()
905        }
906    }
907
908    /// Theme.
909    pub fn theme(&self) -> Theme {
910        Theme::Dark
911    }
912}
913
914impl Default for WaveSimApp {
915    fn default() -> Self {
916        Self::new().0
917    }
918}
919
920/// Run the application.
921pub fn run() -> iced::Result {
922    iced::application(WaveSimApp::title, WaveSimApp::update, WaveSimApp::view)
923        .subscription(WaveSimApp::subscription)
924        .theme(WaveSimApp::theme)
925        .window_size(Size::new(1200.0, 800.0))
926        .antialiasing(true)
927        .run_with(WaveSimApp::new)
928}