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                let c2 = params.courant_number().powi(2);
310                let damping = 1.0 - params.damping;
311                self.courant_number = params.courant_number();
312
313                match &mut self.engine {
314                    SimulationEngine::Cpu(grid) => {
315                        grid.set_speed_of_sound(speed);
316                    }
317                    SimulationEngine::Gpu(kernel_grid) => {
318                        let grid = kernel_grid.clone();
319                        tokio::spawn(async move {
320                            grid.lock().await.set_speed_of_sound(speed);
321                        });
322                    }
323                    #[cfg(feature = "cuda")]
324                    SimulationEngine::CudaPacked(backend) => {
325                        if let Ok(mut b) = backend.lock() {
326                            b.set_params(c2, damping);
327                        }
328                    }
329                    SimulationEngine::Switching => {}
330                }
331            }
332
333            Message::GridWidthChanged(s) => {
334                self.grid_width_input = s;
335            }
336
337            Message::GridHeightChanged(s) => {
338                self.grid_height_input = s;
339            }
340
341            Message::ApplyGridSize => {
342                if let (Ok(w), Ok(h)) = (
343                    self.grid_width_input.parse::<u32>(),
344                    self.grid_height_input.parse::<u32>(),
345                ) {
346                    // Allow larger grids for CUDA Packed
347                    #[cfg(feature = "cuda")]
348                    let max_size = if matches!(self.compute_backend, ComputeBackend::CudaPacked) {
349                        512
350                    } else {
351                        256
352                    };
353                    #[cfg(not(feature = "cuda"))]
354                    let max_size = 256;
355
356                    let w = w.clamp(16, max_size);
357                    let h = h.clamp(16, max_size);
358                    self.grid_width = w;
359                    self.grid_height = h;
360                    self.grid_width_input = w.to_string();
361                    self.grid_height_input = h.to_string();
362                    self.cell_count = (w * h) as usize;
363                    self.reset_performance_stats();
364
365                    // Recreate the engine with new size
366                    return self.switch_backend(self.compute_backend);
367                }
368            }
369
370            Message::ImpulseAmplitudeChanged(amp) => {
371                self.impulse_amplitude = amp;
372            }
373
374            Message::CellSizeChanged(size) => {
375                self.cell_size = size;
376                let params = AcousticParams::new(self.speed_of_sound, size);
377                let c2 = params.courant_number().powi(2);
378                let damping = 1.0 - params.damping;
379                self.courant_number = params.courant_number();
380
381                match &mut self.engine {
382                    SimulationEngine::Cpu(grid) => {
383                        grid.set_cell_size(size);
384                    }
385                    SimulationEngine::Gpu(kernel_grid) => {
386                        let grid = kernel_grid.clone();
387                        tokio::spawn(async move {
388                            grid.lock().await.set_cell_size(size);
389                        });
390                    }
391                    #[cfg(feature = "cuda")]
392                    SimulationEngine::CudaPacked(backend) => {
393                        if let Ok(mut b) = backend.lock() {
394                            b.set_params(c2, damping);
395                        }
396                    }
397                    SimulationEngine::Switching => {}
398                }
399            }
400
401            Message::ComputeBackendChanged(new_backend) => {
402                if new_backend == self.compute_backend {
403                    return Task::none();
404                }
405                tracing::info!(
406                    "Switching compute backend from {} to {}",
407                    self.compute_backend,
408                    new_backend
409                );
410                self.compute_backend = new_backend;
411                self.reset_performance_stats();
412                return self.switch_backend(new_backend);
413            }
414
415            Message::BackendSwitched(result) => match result {
416                Ok(kernel_grid) => {
417                    self.engine = SimulationEngine::Gpu(kernel_grid.clone());
418                    return Task::perform(
419                        async move {
420                            let g = kernel_grid.lock().await;
421                            (g.get_pressure_grid(), g.max_pressure(), g.total_energy())
422                        },
423                        |(pressure, max_p, energy)| {
424                            Message::GpuStepCompleted(pressure, max_p, energy)
425                        },
426                    );
427                }
428                Err(e) => {
429                    tracing::error!("Failed to switch backend: {}", e);
430                    self.compute_backend = ComputeBackend::Cpu;
431                    let params = AcousticParams::new(self.speed_of_sound, self.cell_size);
432                    let grid = SimulationGrid::new(self.grid_width, self.grid_height, params);
433                    self.cell_count = grid.cell_count();
434                    self.courant_number = grid.params.courant_number();
435                    let pressure = grid.get_pressure_grid();
436                    self.engine = SimulationEngine::Cpu(grid);
437                    self.canvas.update_pressure(pressure);
438                }
439            },
440
441            #[cfg(feature = "cuda")]
442            Message::CudaPackedSwitched(result) => {
443                match result {
444                    Ok(backend) => {
445                        self.engine = SimulationEngine::CudaPacked(backend.clone());
446                        // Read initial state
447                        let pressure = {
448                            let b = backend.lock().unwrap();
449                            b.read_pressure_grid()
450                                .unwrap_or_else(|_| vec![0.0; self.cell_count])
451                        };
452                        self.update_canvas_from_flat(&pressure);
453                    }
454                    Err(e) => {
455                        tracing::error!("Failed to create CUDA Packed backend: {}", e);
456                        self.compute_backend = ComputeBackend::Cpu;
457                        let params = AcousticParams::new(self.speed_of_sound, self.cell_size);
458                        let grid = SimulationGrid::new(self.grid_width, self.grid_height, params);
459                        self.cell_count = grid.cell_count();
460                        self.courant_number = grid.params.courant_number();
461                        let pressure = grid.get_pressure_grid();
462                        self.engine = SimulationEngine::Cpu(grid);
463                        self.canvas.update_pressure(pressure);
464                    }
465                }
466            }
467
468            Message::CanvasClick(x, y) => {
469                let gx = (x * self.grid_width as f32) as u32;
470                let gy = (y * self.grid_height as f32) as u32;
471                let gx = gx.min(self.grid_width - 1);
472                let gy = gy.min(self.grid_height - 1);
473
474                // Handle based on draw mode
475                match self.draw_mode {
476                    DrawMode::Impulse => {
477                        // Original impulse behavior
478                        match &self.engine {
479                            SimulationEngine::Cpu(_) => {
480                                if let SimulationEngine::Cpu(grid) = &mut self.engine {
481                                    grid.inject_impulse(gx, gy, self.impulse_amplitude);
482                                    let pressure_grid = grid.get_pressure_grid();
483                                    self.max_pressure = grid.max_pressure();
484                                    self.total_energy = grid.total_energy();
485                                    self.canvas.update_pressure(pressure_grid);
486                                }
487                            }
488                            SimulationEngine::Gpu(kernel_grid) => {
489                                let grid = kernel_grid.clone();
490                                let amp = self.impulse_amplitude;
491                                return Task::perform(
492                                    async move {
493                                        let mut g = grid.lock().await;
494                                        g.inject_impulse(gx, gy, amp);
495                                        (g.get_pressure_grid(), g.max_pressure(), g.total_energy())
496                                    },
497                                    |(pressure, max_p, energy)| {
498                                        Message::GpuStepCompleted(pressure, max_p, energy)
499                                    },
500                                );
501                            }
502                            #[cfg(feature = "cuda")]
503                            SimulationEngine::CudaPacked(backend) => {
504                                let backend = backend.clone();
505                                let amp = self.impulse_amplitude;
506                                return Task::perform(
507                                    async move {
508                                        let b = backend.lock().unwrap();
509                                        let _ = b.inject_impulse(gx, gy, amp);
510                                        let pressure = b.read_pressure_grid().unwrap_or_default();
511                                        (pressure, 0u32)
512                                    },
513                                    |(pressure, steps)| {
514                                        Message::CudaPackedStepCompleted(pressure, steps)
515                                    },
516                                );
517                            }
518                            SimulationEngine::Switching => {}
519                        }
520                    }
521                    DrawMode::Absorber | DrawMode::Reflector | DrawMode::Erase => {
522                        // Set cell type
523                        let cell_type = match self.draw_mode {
524                            DrawMode::Absorber => CellType::Absorber,
525                            DrawMode::Reflector => CellType::Reflector,
526                            DrawMode::Erase => CellType::Normal,
527                            _ => CellType::Normal,
528                        };
529                        self.set_cell_type_at(gx, gy, cell_type);
530                    }
531                }
532            }
533
534            Message::CanvasRightClick(x, y) => {
535                // Right-click always sets cell type based on current draw mode
536                let gx = (x * self.grid_width as f32) as u32;
537                let gy = (y * self.grid_height as f32) as u32;
538                let gx = gx.min(self.grid_width - 1);
539                let gy = gy.min(self.grid_height - 1);
540
541                let cell_type = match self.draw_mode {
542                    DrawMode::Impulse => CellType::Normal, // Right-click in impulse mode erases
543                    DrawMode::Absorber => CellType::Absorber,
544                    DrawMode::Reflector => CellType::Reflector,
545                    DrawMode::Erase => CellType::Normal,
546                };
547                self.set_cell_type_at(gx, gy, cell_type);
548            }
549
550            Message::DrawModeChanged(mode) => {
551                self.draw_mode = mode;
552            }
553
554            Message::ClearCellTypes => {
555                // Clear all cell types
556                for row in &mut self.cell_types {
557                    for cell in row {
558                        *cell = CellType::Normal;
559                    }
560                }
561                // Also clear in the CPU grid if applicable
562                if let SimulationEngine::Cpu(grid) = &mut self.engine {
563                    grid.clear_cell_types();
564                }
565                self.canvas.update_cell_types(self.cell_types.clone());
566            }
567
568            Message::Tick => {
569                if self.is_running {
570                    return self.step_simulation();
571                }
572            }
573
574            Message::GpuStepCompleted(pressure_grid, max_pressure, total_energy) => {
575                self.canvas.update_pressure(pressure_grid);
576                self.max_pressure = max_pressure;
577                self.total_energy = total_energy;
578                self.update_frame_stats(self.steps_last_frame);
579            }
580
581            #[cfg(feature = "cuda")]
582            Message::CudaPackedStepCompleted(pressure, steps) => {
583                self.update_canvas_from_flat(&pressure);
584                self.update_frame_stats(steps);
585            }
586
587            Message::ToggleStats => {
588                self.show_stats = !self.show_stats;
589            }
590
591            Message::SimulationModeChanged(mode) => {
592                self.simulation_mode = mode;
593                self.educational_processor.set_mode(mode);
594                tracing::info!("Simulation mode changed to: {}", mode);
595            }
596        }
597
598        Task::none()
599    }
600
601    /// Switch to a new compute backend.
602    fn switch_backend(&mut self, backend: ComputeBackend) -> Task<Message> {
603        match backend {
604            ComputeBackend::Cpu => {
605                let params = AcousticParams::new(self.speed_of_sound, self.cell_size);
606                let grid = SimulationGrid::new(self.grid_width, self.grid_height, params);
607                self.cell_count = grid.cell_count();
608                self.courant_number = grid.params.courant_number();
609                let pressure = grid.get_pressure_grid();
610                self.engine = SimulationEngine::Cpu(grid);
611                self.canvas.update_pressure(pressure);
612                Task::none()
613            }
614            ComputeBackend::GpuActor => {
615                self.engine = SimulationEngine::Switching;
616                let width = self.grid_width;
617                let height = self.grid_height;
618                let speed = self.speed_of_sound;
619                let cell_size = self.cell_size;
620                let legacy_backend = self.legacy_backend;
621
622                Task::perform(
623                    async move {
624                        let params = AcousticParams::new(speed, cell_size);
625                        match KernelGrid::new(width, height, params, legacy_backend).await {
626                            Ok(grid) => Ok(Arc::new(Mutex::new(grid))),
627                            Err(e) => Err(format!("{:?}", e)),
628                        }
629                    },
630                    Message::BackendSwitched,
631                )
632            }
633            #[cfg(feature = "cuda")]
634            ComputeBackend::CudaPacked => self.switch_to_cuda_packed(),
635        }
636    }
637
638    /// Switch to CUDA Packed backend.
639    #[cfg(feature = "cuda")]
640    fn switch_to_cuda_packed(&mut self) -> Task<Message> {
641        self.engine = SimulationEngine::Switching;
642        let width = self.grid_width;
643        let height = self.grid_height;
644        let speed = self.speed_of_sound;
645        let cell_size = self.cell_size;
646
647        Task::perform(
648            async move {
649                let params = AcousticParams::new(speed, cell_size);
650                let c2 = params.courant_number().powi(2);
651                let damping = 1.0 - params.damping;
652
653                match CudaPackedBackend::new(width, height, 16) {
654                    Ok(mut backend) => {
655                        backend.set_params(c2, damping);
656                        Ok(Arc::new(std::sync::Mutex::new(backend)))
657                    }
658                    Err(e) => Err(format!("{:?}", e)),
659                }
660            },
661            Message::CudaPackedSwitched,
662        )
663    }
664
665    /// Reset performance statistics.
666    fn reset_performance_stats(&mut self) {
667        self.fps = 0.0;
668        self.steps_per_sec = 0.0;
669        self.throughput = 0.0;
670        self.steps_last_frame = 0;
671        self.step_accumulator = 0;
672        self.time_accumulator = Duration::ZERO;
673        self.last_stats_update = Instant::now();
674    }
675
676    /// Update frame statistics after a step completes.
677    fn update_frame_stats(&mut self, steps: u32) {
678        let now = Instant::now();
679        let delta = now.duration_since(self.last_frame);
680        self.fps = 1.0 / delta.as_secs_f32();
681        self.last_frame = now;
682        self.steps_last_frame = steps;
683
684        // Accumulate for averaging
685        self.step_accumulator += steps;
686        self.time_accumulator += delta;
687
688        // Update stats every 500ms
689        let stats_delta = now.duration_since(self.last_stats_update);
690        if stats_delta >= Duration::from_millis(500) {
691            let secs = self.time_accumulator.as_secs_f64();
692            if secs > 0.0 {
693                self.steps_per_sec = self.step_accumulator as f32 / secs as f32;
694                self.throughput = (self.step_accumulator as f64 * self.cell_count as f64) / secs;
695            }
696            self.step_accumulator = 0;
697            self.time_accumulator = Duration::ZERO;
698            self.last_stats_update = now;
699        }
700    }
701
702    /// Update canvas from flat pressure array.
703    fn update_canvas_from_flat(&mut self, pressure: &[f32]) {
704        let mut grid = Vec::with_capacity(self.grid_height as usize);
705        for y in 0..self.grid_height as usize {
706            let start = y * self.grid_width as usize;
707            let end = start + self.grid_width as usize;
708            if end <= pressure.len() {
709                grid.push(pressure[start..end].to_vec());
710            } else {
711                grid.push(vec![0.0; self.grid_width as usize]);
712            }
713        }
714
715        // Calculate max pressure and energy
716        self.max_pressure = pressure.iter().fold(0.0f32, |acc, &p| acc.max(p.abs()));
717        self.total_energy = pressure.iter().map(|&p| p * p).sum();
718
719        self.canvas.update_pressure(grid);
720    }
721
722    /// Set cell type at the given grid position.
723    fn set_cell_type_at(&mut self, gx: u32, gy: u32, cell_type: CellType) {
724        let gx = gx as usize;
725        let gy = gy as usize;
726
727        // Resize cell_types if needed
728        let height = self.grid_height as usize;
729        let width = self.grid_width as usize;
730        if self.cell_types.len() != height || (height > 0 && self.cell_types[0].len() != width) {
731            self.cell_types = vec![vec![CellType::Normal; width]; height];
732        }
733
734        // Update local cell_types array
735        if gy < self.cell_types.len() && gx < self.cell_types[gy].len() {
736            self.cell_types[gy][gx] = cell_type;
737        }
738
739        // Update simulation grid for CPU mode
740        if let SimulationEngine::Cpu(grid) = &mut self.engine {
741            grid.set_cell_type(gx as u32, gy as u32, cell_type);
742        }
743
744        // Update canvas to show cell types
745        self.canvas.update_cell_types(self.cell_types.clone());
746    }
747
748    /// Perform simulation step(s).
749    fn step_simulation(&mut self) -> Task<Message> {
750        match &self.engine {
751            SimulationEngine::Cpu(_) => {
752                let start = Instant::now();
753                if let SimulationEngine::Cpu(grid) = &mut self.engine {
754                    // Check if we're using educational mode
755                    if self.simulation_mode != SimulationMode::Standard {
756                        // Educational mode: use the educational processor
757                        let (pressure, pressure_prev, width, height, c2, damping) =
758                            grid.get_buffers_mut();
759
760                        let result = self.educational_processor.step_frame(
761                            pressure,
762                            pressure_prev,
763                            width,
764                            height,
765                            c2,
766                            damping,
767                        );
768
769                        if result.step_complete && result.should_swap {
770                            grid.swap_buffers();
771                        }
772
773                        // Update processing indicators on canvas
774                        self.canvas.set_processing_state(
775                            self.educational_processor.state.just_processed.clone(),
776                            self.educational_processor.state.active_tiles.clone(),
777                            self.educational_processor.state.current_row,
778                        );
779
780                        self.steps_last_frame = if result.step_complete { 1 } else { 0 };
781                    } else {
782                        // Standard mode: run multiple steps per frame
783                        let target_frame_time = Duration::from_millis(16);
784                        let dt = grid.params.time_step;
785                        let max_steps = ((0.01 / dt) as u32).clamp(1, 2000);
786
787                        let mut steps = 0u32;
788                        while start.elapsed() < target_frame_time && steps < max_steps {
789                            grid.step();
790                            steps += 1;
791                        }
792
793                        self.steps_last_frame = steps;
794                        // Clear processing indicators
795                        self.canvas.set_processing_state(vec![], vec![], None);
796                    }
797
798                    let pressure_grid = grid.get_pressure_grid();
799                    self.max_pressure = grid.max_pressure();
800                    self.total_energy = grid.total_energy();
801                    self.canvas.update_pressure(pressure_grid);
802                    self.update_frame_stats(self.steps_last_frame);
803                }
804                Task::none()
805            }
806            SimulationEngine::Gpu(kernel_grid) => {
807                let grid = kernel_grid.clone();
808
809                Task::perform(
810                    async move {
811                        let mut g = grid.lock().await;
812                        let dt = g.params.time_step;
813                        let steps = ((0.01 / dt) as u32).clamp(1, 500);
814
815                        for _ in 0..steps {
816                            if let Err(e) = g.step().await {
817                                tracing::error!("GPU step error: {:?}", e);
818                                break;
819                            }
820                        }
821                        (g.get_pressure_grid(), g.max_pressure(), g.total_energy())
822                    },
823                    |(pressure, max_p, energy)| Message::GpuStepCompleted(pressure, max_p, energy),
824                )
825            }
826            #[cfg(feature = "cuda")]
827            SimulationEngine::CudaPacked(backend) => {
828                let backend = backend.clone();
829                let cell_count = self.cell_count;
830
831                Task::perform(
832                    async move {
833                        let mut b = backend.lock().unwrap();
834                        // Run many steps per frame for CUDA Packed (it's fast!)
835                        let steps = 100u32;
836                        if let Err(e) = b.step_batch(steps) {
837                            tracing::error!("CUDA step error: {:?}", e);
838                        }
839                        let pressure = b
840                            .read_pressure_grid()
841                            .unwrap_or_else(|_| vec![0.0; cell_count]);
842                        (pressure, steps)
843                    },
844                    |(pressure, steps)| Message::CudaPackedStepCompleted(pressure, steps),
845                )
846            }
847            SimulationEngine::Switching => Task::none(),
848        }
849    }
850
851    /// Build the view.
852    pub fn view(&self) -> Element<'_, Message> {
853        let canvas = Canvas::new(&self.canvas)
854            .width(Length::Fill)
855            .height(Length::Fill);
856
857        let controls = controls::view_controls(
858            self.is_running,
859            self.speed_of_sound,
860            self.cell_size,
861            &self.grid_width_input,
862            &self.grid_height_input,
863            self.impulse_amplitude,
864            self.compute_backend,
865            self.draw_mode,
866            self.simulation_mode,
867            self.show_stats,
868            self.fps,
869            self.steps_per_sec,
870            self.throughput,
871            self.cell_count,
872            self.courant_number,
873            self.max_pressure,
874            self.total_energy,
875        );
876
877        let content = row![
878            container(canvas)
879                .width(Length::FillPortion(3))
880                .height(Length::Fill)
881                .padding(10),
882            container(controls)
883                .width(Length::FillPortion(1))
884                .height(Length::Fill)
885                .padding(10),
886        ];
887
888        container(content)
889            .width(Length::Fill)
890            .height(Length::Fill)
891            .into()
892    }
893
894    /// Subscriptions for animation.
895    pub fn subscription(&self) -> Subscription<Message> {
896        if self.is_running {
897            iced::time::every(Duration::from_millis(16)).map(|_| Message::Tick)
898        } else {
899            Subscription::none()
900        }
901    }
902
903    /// Theme.
904    pub fn theme(&self) -> Theme {
905        Theme::Dark
906    }
907}
908
909impl Default for WaveSimApp {
910    fn default() -> Self {
911        Self::new().0
912    }
913}
914
915/// Run the application.
916pub fn run() -> iced::Result {
917    iced::application(WaveSimApp::title, WaveSimApp::update, WaveSimApp::view)
918        .subscription(WaveSimApp::subscription)
919        .theme(WaveSimApp::theme)
920        .window_size(Size::new(1200.0, 800.0))
921        .antialiasing(true)
922        .run_with(WaveSimApp::new)
923}