Skip to main content

anvilkit_render/window/
events.rs

1//! # 事件处理和应用生命周期
2//!
3//! 基于 winit 0.30 的 ApplicationHandler 实现应用生命周期管理和事件处理。
4
5use std::sync::Arc;
6use std::time::Instant;
7use winit::{
8    application::ApplicationHandler,
9    event::{WindowEvent, DeviceEvent, DeviceId},
10    event_loop::ActiveEventLoop,
11    window::{Window, WindowId},
12    dpi::PhysicalSize,
13};
14use log::{info, error, debug};
15
16use anvilkit_ecs::app::App;
17use anvilkit_ecs::physics::DeltaTime;
18use anvilkit_input::prelude::{InputState, KeyCode, MouseButton};
19use crate::window::{WindowConfig, WindowState};
20use crate::renderer::{RenderDevice, RenderSurface};
21use crate::renderer::assets::RenderAssets;
22use crate::renderer::draw::{ActiveCamera, DrawCommandList, SceneLights};
23use crate::renderer::state::{RenderState, PbrSceneUniform, GpuLight, MAX_LIGHTS};
24use crate::renderer::buffer::{
25    create_uniform_buffer, create_depth_texture_msaa,
26    create_hdr_render_target, create_hdr_msaa_texture,
27    create_sampler, create_texture_linear, create_shadow_map, create_shadow_sampler,
28    Vertex, PbrVertex, SHADOW_MAP_SIZE,
29};
30use crate::renderer::{RenderPipelineBuilder, DEPTH_FORMAT};
31use crate::renderer::ibl::generate_brdf_lut;
32use anvilkit_core::error::{AnvilKitError, Result};
33
34/// 将 SceneLights 打包为 GPU 光源数组
35///
36/// 返回 (lights_array, light_count)。方向光占 slot 0,其余填充点光和聚光。
37/// 可被游戏和示例直接调用,不必复制此函数。
38pub fn pack_lights(scene_lights: &SceneLights) -> ([GpuLight; MAX_LIGHTS], u32) {
39    let mut lights = [GpuLight::default(); MAX_LIGHTS];
40    let mut count = 0u32;
41
42    // Slot 0: directional light (type=0)
43    let dir = &scene_lights.directional;
44    lights[0] = GpuLight {
45        position_type: [0.0, 0.0, 0.0, 0.0], // type=0 directional
46        direction_range: [dir.direction.x, dir.direction.y, dir.direction.z, 0.0],
47        color_intensity: [dir.color.x, dir.color.y, dir.color.z, dir.intensity],
48        params: [0.0; 4],
49    };
50    count += 1;
51
52    // Point lights (type=1)
53    for pl in &scene_lights.point_lights {
54        if count as usize >= MAX_LIGHTS { break; }
55        lights[count as usize] = GpuLight {
56            position_type: [pl.position.x, pl.position.y, pl.position.z, 1.0],
57            direction_range: [0.0, 0.0, 0.0, pl.range],
58            color_intensity: [pl.color.x, pl.color.y, pl.color.z, pl.intensity],
59            params: [0.0; 4],
60        };
61        count += 1;
62    }
63
64    // Spot lights (type=2)
65    for sl in &scene_lights.spot_lights {
66        if count as usize >= MAX_LIGHTS { break; }
67        lights[count as usize] = GpuLight {
68            position_type: [sl.position.x, sl.position.y, sl.position.z, 2.0],
69            direction_range: [sl.direction.x, sl.direction.y, sl.direction.z, sl.range],
70            color_intensity: [sl.color.x, sl.color.y, sl.color.z, sl.intensity],
71            params: [sl.inner_cone_angle.cos(), sl.outer_cone_angle.cos(), 0.0, 0.0],
72        };
73        count += 1;
74    }
75
76    (lights, count)
77}
78
79/// 计算方向光的光空间矩阵(正交投影)
80///
81/// 生成一个从光源方向看向原点的 view-projection 矩阵,
82/// 用于 shadow pass 的深度渲染。
83pub fn compute_light_space_matrix(light_direction: &glam::Vec3) -> glam::Mat4 {
84    let light_dir = light_direction.normalize();
85    // 光源位置设在场景中心的反方向
86    let light_pos = -light_dir * 15.0;
87    let light_view = glam::Mat4::look_at_lh(light_pos, glam::Vec3::ZERO, glam::Vec3::Y);
88    // 正交投影覆盖场景范围
89    let light_proj = glam::Mat4::orthographic_lh(-10.0, 10.0, -10.0, 10.0, 0.1, 30.0);
90    light_proj * light_view
91}
92
93/// Shadow pass shader (depth-only, reads model + view_proj from scene uniform)
94const SHADOW_SHADER: &str = include_str!("../shaders/shadow.wgsl");
95
96/// ACES Filmic tone mapping post-process shader (fullscreen triangle)
97const TONEMAP_SHADER: &str = include_str!("../shaders/tonemap.wgsl");
98
99/// 渲染应用
100///
101/// 实现 ApplicationHandler trait,管理窗口生命周期和渲染循环。
102/// 使用 winit 0.30 API 设计,提供跨平台兼容性。
103///
104/// # 设计理念
105///
106/// - **延迟初始化**: 在 `resumed` 事件中创建窗口和渲染上下文
107/// - **事件驱动**: 响应窗口事件和设备事件
108/// - **ECS 集成**: 持有 App 并每帧调用 update()
109///
110/// # 示例
111///
112/// ```rust,no_run
113/// use anvilkit_render::window::{RenderApp, WindowConfig};
114/// use anvilkit_render::prelude::*;
115/// use winit::event_loop::EventLoop;
116///
117/// let mut app = App::new();
118/// app.add_plugins(RenderPlugin::default());
119///
120/// RenderApp::run(app);
121/// ```
122pub struct RenderApp {
123    /// 窗口配置
124    config: WindowConfig,
125    /// 窗口实例(延迟初始化)
126    window: Option<Arc<Window>>,
127    /// 窗口状态
128    window_state: WindowState,
129    /// 渲染设备(延迟初始化)
130    render_device: Option<RenderDevice>,
131    /// 渲染表面(延迟初始化,内部持有 Arc<Window>)
132    render_surface: Option<RenderSurface>,
133
134    /// 是否请求退出
135    exit_requested: bool,
136
137    // --- ECS fields ---
138    /// ECS App(当通过 RenderApp::run() 启动时持有)
139    app: Option<App>,
140    /// GPU 是否已初始化并注入到 ECS World
141    gpu_initialized: bool,
142
143    /// 上一帧时间戳,用于计算真实帧时间
144    last_frame_time: Instant,
145}
146
147impl RenderApp {
148    /// 创建新的渲染应用
149    ///
150    /// # 参数
151    ///
152    /// - `config`: 窗口配置参数
153    ///
154    /// # 示例
155    ///
156    /// ```rust
157    /// use anvilkit_render::window::{RenderApp, WindowConfig};
158    ///
159    /// let config = WindowConfig::new().with_title("我的应用");
160    /// let app = RenderApp::new(config);
161    /// ```
162    pub fn new(config: WindowConfig) -> Self {
163        info!("创建渲染应用: {}", config.title);
164
165        Self {
166            config,
167            window: None,
168            window_state: WindowState::new(),
169            render_device: None,
170            render_surface: None,
171            exit_requested: false,
172            app: None,
173            gpu_initialized: false,
174            last_frame_time: Instant::now(),
175        }
176    }
177
178    /// ECS 驱动的入口点
179    ///
180    /// 创建 EventLoop、窗口,运行 winit 主循环。
181    /// 每帧调用 `app.update()` 然后执行 GPU 渲染。
182    ///
183    /// # 参数
184    ///
185    /// - `app`: 已配置好 RenderPlugin 和系统的 ECS App
186    pub fn run(app: App) {
187        let event_loop = winit::event_loop::EventLoop::new().unwrap();
188
189        // 从 App 中读取 RenderConfig 获取 WindowConfig
190        let window_config = app.world.get_resource::<crate::plugin::RenderConfig>()
191            .map(|c| c.window_config.clone())
192            .unwrap_or_default();
193
194        let mut render_app = Self::new(window_config);
195        render_app.app = Some(app);
196
197        event_loop.run_app(&mut render_app).unwrap();
198    }
199
200    /// 获取窗口配置
201    pub fn config(&self) -> &WindowConfig {
202        &self.config
203    }
204
205    /// 获取窗口状态
206    pub fn window_state(&self) -> &WindowState {
207        &self.window_state
208    }
209
210    /// 获取窗口实例
211    pub fn window(&self) -> Option<&Arc<Window>> {
212        self.window.as_ref()
213    }
214
215    /// 请求退出应用
216    pub fn request_exit(&mut self) {
217        info!("请求退出应用");
218        self.exit_requested = true;
219    }
220
221    /// 检查是否请求退出
222    pub fn is_exit_requested(&self) -> bool {
223        self.exit_requested
224    }
225
226    /// 获取渲染设备(初始化后可用)
227    pub fn render_device(&self) -> Option<&RenderDevice> {
228        self.render_device.as_ref()
229    }
230
231    /// 获取渲染表面格式(初始化后可用)
232    pub fn surface_format(&self) -> Option<wgpu::TextureFormat> {
233        self.render_surface.as_ref().map(|s| s.format())
234    }
235
236    /// 获取当前帧的 SurfaceTexture(用于外部渲染)
237    pub fn get_current_frame(&self) -> Option<wgpu::SurfaceTexture> {
238        self.render_surface.as_ref().and_then(|s| s.get_current_frame().ok())
239    }
240
241    // --- Internal methods ---
242
243    /// 创建窗口
244    fn create_window(&mut self, event_loop: &ActiveEventLoop) -> Result<()> {
245        if self.window.is_some() {
246            return Ok(());
247        }
248
249        info!("创建窗口: {} ({}x{})",
250              self.config.title, self.config.width, self.config.height);
251
252        let attributes = self.config.to_window_attributes();
253        let window = event_loop.create_window(attributes)
254            .map_err(|e| AnvilKitError::render(format!("创建窗口失败: {}", e)))?;
255
256        let size = window.inner_size();
257        self.window_state.set_size(size.width, size.height);
258        self.window_state.set_scale_factor(window.scale_factor());
259
260        self.window = Some(Arc::new(window));
261
262        info!("窗口创建成功");
263        Ok(())
264    }
265
266    /// 初始化渲染资源
267    async fn init_render(&mut self) -> Result<()> {
268        if self.render_device.is_some() {
269            return Ok(());
270        }
271
272        let window = self.window.as_ref()
273            .ok_or_else(|| AnvilKitError::render("窗口未创建".to_string()))?;
274
275        info!("初始化渲染设备和表面");
276
277        let device = RenderDevice::new(window).await?;
278        let surface = RenderSurface::new_with_vsync(&device, window, self.config.vsync)?;
279
280        self.render_device = Some(device);
281        self.render_surface = Some(surface);
282
283        info!("渲染设备和表面初始化成功");
284        Ok(())
285    }
286
287    /// GPU 初始化后,将共享资源注入 ECS World
288    fn inject_render_state_to_ecs(&mut self) {
289        if self.gpu_initialized {
290            return;
291        }
292
293        let Some(app) = &mut self.app else { return };
294        let Some(device) = &self.render_device else { return };
295        let Some(surface) = &self.render_surface else { return };
296
297        let format = surface.format();
298        let (w, h) = self.window_state.size();
299
300        // 创建 PBR 场景 Uniform 缓冲区 (256 字节)
301        let initial_uniform = PbrSceneUniform::default();
302        let scene_uniform_buffer = create_uniform_buffer(
303            device,
304            "ECS Scene Uniform",
305            bytemuck::bytes_of(&initial_uniform),
306        );
307
308        let scene_bind_group_layout = device.device().create_bind_group_layout(
309            &wgpu::BindGroupLayoutDescriptor {
310                label: Some("ECS Scene BGL"),
311                entries: &[wgpu::BindGroupLayoutEntry {
312                    binding: 0,
313                    visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
314                    ty: wgpu::BindingType::Buffer {
315                        ty: wgpu::BufferBindingType::Uniform,
316                        has_dynamic_offset: false,
317                        min_binding_size: None,
318                    },
319                    count: None,
320                }],
321            },
322        );
323
324        let scene_bind_group = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
325            label: Some("ECS Scene BG"),
326            layout: &scene_bind_group_layout,
327            entries: &[wgpu::BindGroupEntry {
328                binding: 0,
329                resource: scene_uniform_buffer.as_entire_binding(),
330            }],
331        });
332
333        let (_, depth_texture_view) = create_depth_texture_msaa(device, w, h, "ECS Depth MSAA");
334
335        // HDR render target (resolve target, sample_count=1) + MSAA color attachment
336        let (_, hdr_texture_view) = create_hdr_render_target(device, w, h, "ECS HDR RT");
337        let (_, hdr_msaa_texture_view) = create_hdr_msaa_texture(device, w, h, "ECS HDR MSAA");
338        let sampler = create_sampler(device, "ECS Tonemap Sampler");
339
340        // Tonemap bind group layout + bind group
341        let tonemap_bind_group_layout = device.device().create_bind_group_layout(
342            &wgpu::BindGroupLayoutDescriptor {
343                label: Some("ECS Tonemap BGL"),
344                entries: &[
345                    wgpu::BindGroupLayoutEntry {
346                        binding: 0, visibility: wgpu::ShaderStages::FRAGMENT,
347                        ty: wgpu::BindingType::Texture {
348                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
349                            view_dimension: wgpu::TextureViewDimension::D2,
350                            multisampled: false,
351                        }, count: None,
352                    },
353                    wgpu::BindGroupLayoutEntry {
354                        binding: 1, visibility: wgpu::ShaderStages::FRAGMENT,
355                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
356                        count: None,
357                    },
358                ],
359            },
360        );
361
362        let tonemap_bind_group = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
363            label: Some("ECS Tonemap BG"),
364            layout: &tonemap_bind_group_layout,
365            entries: &[
366                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&hdr_texture_view) },
367                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
368            ],
369        });
370
371        // Tonemap pipeline needs its own bind group layout (consumed by builder)
372        let tonemap_pipeline_bgl = device.device().create_bind_group_layout(
373            &wgpu::BindGroupLayoutDescriptor {
374                label: Some("ECS Tonemap Pipeline BGL"),
375                entries: &[
376                    wgpu::BindGroupLayoutEntry {
377                        binding: 0, visibility: wgpu::ShaderStages::FRAGMENT,
378                        ty: wgpu::BindingType::Texture {
379                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
380                            view_dimension: wgpu::TextureViewDimension::D2,
381                            multisampled: false,
382                        }, count: None,
383                    },
384                    wgpu::BindGroupLayoutEntry {
385                        binding: 1, visibility: wgpu::ShaderStages::FRAGMENT,
386                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
387                        count: None,
388                    },
389                ],
390            },
391        );
392
393        let tonemap_pipeline = RenderPipelineBuilder::new()
394            .with_vertex_shader(TONEMAP_SHADER)
395            .with_fragment_shader(TONEMAP_SHADER)
396            .with_format(format)
397            .with_vertex_layouts(vec![])
398            .with_bind_group_layouts(vec![tonemap_pipeline_bgl])
399            .with_label("ECS Tonemap Pipeline")
400            .build(device)
401            .expect("创建 Tonemap 管线失败")
402            .into_pipeline();
403
404        // IBL + Shadow: bind group 2 (BRDF LUT + shadow map)
405        let brdf_lut_data = generate_brdf_lut(256);
406        let (_, brdf_lut_view) = create_texture_linear(device, 256, 256, &brdf_lut_data, "ECS BRDF LUT");
407        let (_, shadow_map_view) = create_shadow_map(device, SHADOW_MAP_SIZE, "ECS Shadow Map");
408        let shadow_sampler = create_shadow_sampler(device, "ECS Shadow Sampler");
409
410        let ibl_shadow_bind_group_layout = device.device().create_bind_group_layout(
411            &wgpu::BindGroupLayoutDescriptor {
412                label: Some("ECS IBL+Shadow BGL"),
413                entries: &[
414                    wgpu::BindGroupLayoutEntry {
415                        binding: 0, visibility: wgpu::ShaderStages::FRAGMENT,
416                        ty: wgpu::BindingType::Texture {
417                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
418                            view_dimension: wgpu::TextureViewDimension::D2,
419                            multisampled: false,
420                        }, count: None,
421                    },
422                    wgpu::BindGroupLayoutEntry {
423                        binding: 1, visibility: wgpu::ShaderStages::FRAGMENT,
424                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
425                        count: None,
426                    },
427                    wgpu::BindGroupLayoutEntry {
428                        binding: 2, visibility: wgpu::ShaderStages::FRAGMENT,
429                        ty: wgpu::BindingType::Texture {
430                            sample_type: wgpu::TextureSampleType::Depth,
431                            view_dimension: wgpu::TextureViewDimension::D2,
432                            multisampled: false,
433                        }, count: None,
434                    },
435                    wgpu::BindGroupLayoutEntry {
436                        binding: 3, visibility: wgpu::ShaderStages::FRAGMENT,
437                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
438                        count: None,
439                    },
440                ],
441            },
442        );
443
444        let ibl_shadow_bind_group = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
445            label: Some("ECS IBL+Shadow BG"),
446            layout: &ibl_shadow_bind_group_layout,
447            entries: &[
448                wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&brdf_lut_view) },
449                wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
450                wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&shadow_map_view) },
451                wgpu::BindGroupEntry { binding: 3, resource: wgpu::BindingResource::Sampler(&shadow_sampler) },
452            ],
453        });
454
455        // Shadow pass pipeline (depth-only, uses PbrVertex layout for position)
456        let shadow_scene_bgl = device.device().create_bind_group_layout(
457            &wgpu::BindGroupLayoutDescriptor {
458                label: Some("Shadow Scene BGL"),
459                entries: &[wgpu::BindGroupLayoutEntry {
460                    binding: 0,
461                    visibility: wgpu::ShaderStages::VERTEX,
462                    ty: wgpu::BindingType::Buffer {
463                        ty: wgpu::BufferBindingType::Uniform,
464                        has_dynamic_offset: false,
465                        min_binding_size: None,
466                    },
467                    count: None,
468                }],
469            },
470        );
471
472        let shadow_pipeline = RenderPipelineBuilder::new()
473            .with_vertex_shader(SHADOW_SHADER)
474            .with_format(wgpu::TextureFormat::Rgba8Unorm) // dummy, no color output
475            .with_vertex_layouts(vec![PbrVertex::layout()])
476            .with_depth_format(DEPTH_FORMAT)
477            .with_bind_group_layouts(vec![shadow_scene_bgl])
478            .with_label("ECS Shadow Pipeline")
479            .build_depth_only(device)
480            .expect("创建 Shadow 管线失败")
481            .into_pipeline();
482
483        app.insert_resource(RenderState {
484            surface_format: format,
485            surface_size: (w, h),
486            scene_uniform_buffer,
487            scene_bind_group,
488            scene_bind_group_layout,
489            depth_texture_view,
490            hdr_texture_view,
491            tonemap_pipeline,
492            tonemap_bind_group,
493            tonemap_bind_group_layout,
494            ibl_shadow_bind_group,
495            ibl_shadow_bind_group_layout,
496            shadow_pipeline,
497            shadow_map_view,
498            hdr_msaa_texture_view,
499        });
500
501        self.gpu_initialized = true;
502        info!("RenderState (HDR + IBL + Shadow) 已注入 ECS World");
503    }
504
505    /// 处理窗口大小变化
506    fn handle_resize(&mut self, new_size: PhysicalSize<u32>) {
507        debug!("窗口大小变化: {}x{}", new_size.width, new_size.height);
508
509        self.window_state.set_size(new_size.width, new_size.height);
510
511        if let (Some(device), Some(surface)) = (&self.render_device, &mut self.render_surface) {
512            if let Err(e) = surface.resize(device, new_size.width, new_size.height) {
513                error!("调整渲染表面大小失败: {}", e);
514            }
515        }
516
517        // 更新 ECS RenderState 中的深度纹理、HDR RT 和 surface_size
518        if self.gpu_initialized && new_size.width > 0 && new_size.height > 0 {
519            if let (Some(app), Some(device)) = (&mut self.app, &self.render_device) {
520                if let Some(mut rs) = app.world.get_resource_mut::<RenderState>() {
521                    rs.surface_size = (new_size.width, new_size.height);
522                    let (_, depth_view) = create_depth_texture_msaa(device, new_size.width, new_size.height, "ECS Depth MSAA");
523                    rs.depth_texture_view = depth_view;
524
525                    // Recreate HDR RT (resolve), MSAA color, and tonemap bind group
526                    let (_, hdr_view) = create_hdr_render_target(device, new_size.width, new_size.height, "ECS HDR RT");
527                    let (_, hdr_msaa_view) = create_hdr_msaa_texture(device, new_size.width, new_size.height, "ECS HDR MSAA");
528                    let sampler = create_sampler(device, "ECS Sampler");
529                    let new_bg = device.device().create_bind_group(&wgpu::BindGroupDescriptor {
530                        label: Some("ECS Tonemap BG"),
531                        layout: &rs.tonemap_bind_group_layout,
532                        entries: &[
533                            wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&hdr_view) },
534                            wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler) },
535                        ],
536                    });
537                    rs.hdr_texture_view = hdr_view;
538                    rs.hdr_msaa_texture_view = hdr_msaa_view;
539                    rs.tonemap_bind_group = new_bg;
540                }
541            }
542        }
543    }
544
545    /// 处理缩放因子变化
546    fn handle_scale_factor_changed(&mut self, scale_factor: f64) {
547        debug!("缩放因子变化: {}", scale_factor);
548        self.window_state.set_scale_factor(scale_factor);
549    }
550
551    /// 执行 ECS 多物体 HDR PBR 渲染
552    ///
553    /// Pass 1: 场景渲染到 HDR RT (Rgba16Float)
554    /// Pass 2: Tone mapping HDR → Swapchain (ACES Filmic)
555    fn render_ecs(&mut self) {
556        let (Some(device), Some(surface)) = (&self.render_device, &self.render_surface) else {
557            return;
558        };
559
560        let Some(app) = &self.app else { return };
561
562        let Some(active_camera) = app.world.get_resource::<ActiveCamera>() else { return };
563        let Some(draw_list) = app.world.get_resource::<DrawCommandList>() else { return };
564        let Some(render_assets) = app.world.get_resource::<RenderAssets>() else { return };
565        let Some(render_state) = app.world.get_resource::<RenderState>() else { return };
566
567        if draw_list.commands.is_empty() {
568            return;
569        }
570
571        let frame = match surface.get_current_frame_with_recovery(device) {
572            Ok(frame) => frame,
573            Err(e) => {
574                error!("获取当前帧失败: {}", e);
575                return;
576            }
577        };
578
579        let swapchain_view = frame.texture.create_view(&Default::default());
580        let view_proj = active_camera.view_proj;
581        let camera_pos = active_camera.camera_pos;
582
583        // 获取场景灯光并打包为 GPU 数组
584        let default_lights = SceneLights::default();
585        let scene_lights = app.world.get_resource::<SceneLights>()
586            .unwrap_or(&default_lights);
587        let (gpu_lights, light_count) = pack_lights(scene_lights);
588        let light = &scene_lights.directional;
589
590        // Compute light-space matrix for shadow mapping (directional light)
591        let shadow_view_proj = compute_light_space_matrix(&light.direction);
592
593        // === Batched rendering: single encoder, multiple passes, single submit ===
594        let mut encoder = device.device().create_command_encoder(
595            &wgpu::CommandEncoderDescriptor { label: Some("ECS Frame Encoder") },
596        );
597
598        // --- Pass 0: Shadow pass → shadow depth map ---
599        {
600            let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
601                label: Some("Shadow Pass"),
602                color_attachments: &[],
603                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
604                    view: &render_state.shadow_map_view,
605                    depth_ops: Some(wgpu::Operations {
606                        load: wgpu::LoadOp::Clear(1.0),
607                        store: wgpu::StoreOp::Store,
608                    }),
609                    stencil_ops: None,
610                }),
611                timestamp_writes: None,
612                occlusion_query_set: None,
613            });
614            rp.set_pipeline(&render_state.shadow_pipeline);
615            rp.set_bind_group(0, &render_state.scene_bind_group, &[]);
616
617            for cmd in draw_list.commands.iter() {
618                let Some(gpu_mesh) = render_assets.get_mesh(&cmd.mesh) else { continue };
619
620                let shadow_uniform = PbrSceneUniform {
621                    model: cmd.model_matrix.to_cols_array_2d(),
622                    view_proj: shadow_view_proj.to_cols_array_2d(),
623                    ..Default::default()
624                };
625                device.queue().write_buffer(
626                    &render_state.scene_uniform_buffer, 0, bytemuck::bytes_of(&shadow_uniform),
627                );
628
629                rp.set_vertex_buffer(0, gpu_mesh.vertex_buffer.slice(..));
630                rp.set_index_buffer(gpu_mesh.index_buffer.slice(..), gpu_mesh.index_format);
631                rp.draw_indexed(0..gpu_mesh.index_count, 0, 0..1);
632            }
633        }
634
635        // --- Pass 1: Scene → HDR render target ---
636        {
637            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
638                label: Some("ECS HDR Scene Pass"),
639                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
640                    view: &render_state.hdr_msaa_texture_view,
641                    resolve_target: Some(&render_state.hdr_texture_view),
642                    ops: wgpu::Operations {
643                        load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.15, g: 0.3, b: 0.6, a: 1.0 }),
644                        store: wgpu::StoreOp::Discard,
645                    },
646                })],
647                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
648                    view: &render_state.depth_texture_view,
649                    depth_ops: Some(wgpu::Operations {
650                        load: wgpu::LoadOp::Clear(1.0),
651                        store: wgpu::StoreOp::Store,
652                    }),
653                    stencil_ops: None,
654                }),
655                timestamp_writes: None,
656                occlusion_query_set: None,
657            });
658
659            for cmd in draw_list.commands.iter() {
660                let Some(gpu_mesh) = render_assets.get_mesh(&cmd.mesh) else { continue };
661                let Some(gpu_material) = render_assets.get_material(&cmd.material) else { continue };
662
663                let model = cmd.model_matrix;
664                // Normal matrix: for uniform scale, transpose == inverse().transpose()
665                // but transpose is O(1) vs inverse is O(n³). Use inverse().transpose()
666                // only when non-uniform scale is detected.
667                let scale = glam::Vec3::new(
668                    model.x_axis.truncate().length(),
669                    model.y_axis.truncate().length(),
670                    model.z_axis.truncate().length(),
671                );
672                let normal_matrix = if (scale.x - scale.y).abs() < 0.001
673                    && (scale.y - scale.z).abs() < 0.001
674                {
675                    // Uniform scale — transpose is sufficient
676                    model.transpose()
677                } else {
678                    // Non-uniform scale — need full inverse transpose
679                    model.inverse().transpose()
680                };
681
682                let uniform = PbrSceneUniform {
683                    model: model.to_cols_array_2d(),
684                    view_proj: view_proj.to_cols_array_2d(),
685                    normal_matrix: normal_matrix.to_cols_array_2d(),
686                    camera_pos: [camera_pos.x, camera_pos.y, camera_pos.z, 0.0],
687                    light_dir: [light.direction.x, light.direction.y, light.direction.z, 0.0],
688                    light_color: [light.color.x, light.color.y, light.color.z, light.intensity],
689                    material_params: [cmd.metallic, cmd.roughness, cmd.normal_scale, light_count as f32],
690                    lights: gpu_lights,
691                    shadow_view_proj: shadow_view_proj.to_cols_array_2d(),
692                    emissive_factor: [cmd.emissive_factor[0], cmd.emissive_factor[1], cmd.emissive_factor[2], 1.0 / SHADOW_MAP_SIZE as f32],
693                };
694                device.queue().write_buffer(
695                    &render_state.scene_uniform_buffer, 0, bytemuck::bytes_of(&uniform),
696                );
697
698                let Some(pipeline) = render_assets.get_pipeline(&gpu_material.pipeline_handle) else {
699                    log::error!("材质引用了不存在的管线");
700                    continue;
701                };
702                render_pass.set_pipeline(pipeline);
703                render_pass.set_bind_group(0, &render_state.scene_bind_group, &[]);
704                render_pass.set_bind_group(1, &gpu_material.bind_group, &[]);
705                render_pass.set_bind_group(2, &render_state.ibl_shadow_bind_group, &[]);
706                render_pass.set_vertex_buffer(0, gpu_mesh.vertex_buffer.slice(..));
707                render_pass.set_index_buffer(gpu_mesh.index_buffer.slice(..), gpu_mesh.index_format);
708                render_pass.draw_indexed(0..gpu_mesh.index_count, 0, 0..1);
709            }
710        }
711
712        // --- Pass 2: Tone mapping HDR → Swapchain ---
713        {
714            let mut rp = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
715                label: Some("ECS Tonemap Pass"),
716                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
717                    view: &swapchain_view,
718                    resolve_target: None,
719                    ops: wgpu::Operations {
720                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
721                        store: wgpu::StoreOp::Store,
722                    },
723                })],
724                depth_stencil_attachment: None,
725                timestamp_writes: None,
726                occlusion_query_set: None,
727            });
728
729            rp.set_pipeline(&render_state.tonemap_pipeline);
730            rp.set_bind_group(0, &render_state.tonemap_bind_group, &[]);
731            rp.draw(0..3, 0..1); // Fullscreen triangle
732        }
733
734        // Single submit for all passes
735        device.queue().submit(std::iter::once(encoder.finish()));
736
737        frame.present();
738    }
739
740    /// 执行渲染(ECS 路径)
741    fn render(&mut self) {
742        if self.app.is_some() && self.gpu_initialized {
743            self.render_ecs();
744        }
745    }
746}
747
748impl ApplicationHandler for RenderApp {
749    /// 应用恢复事件
750    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
751        info!("应用恢复");
752
753        if let Err(e) = self.create_window(event_loop) {
754            error!("创建窗口失败: {}", e);
755            event_loop.exit();
756            return;
757        }
758
759        if let Err(e) = pollster::block_on(self.init_render()) {
760            error!("初始化渲染失败: {}", e);
761            event_loop.exit();
762            return;
763        }
764
765        // 如果持有 ECS App,注入 RenderState
766        self.inject_render_state_to_ecs();
767
768        if let Some(window) = &self.window {
769            window.request_redraw();
770        }
771    }
772
773    /// 窗口事件处理
774    fn window_event(
775        &mut self,
776        event_loop: &ActiveEventLoop,
777        _window_id: WindowId,
778        event: WindowEvent,
779    ) {
780        match event {
781            WindowEvent::CloseRequested => {
782                info!("收到窗口关闭请求");
783                self.request_exit();
784                event_loop.exit();
785            }
786
787            WindowEvent::Resized(new_size) => {
788                self.handle_resize(new_size);
789            }
790
791            WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
792                self.handle_scale_factor_changed(scale_factor);
793            }
794
795            WindowEvent::KeyboardInput { event, .. } => {
796                if let winit::keyboard::PhysicalKey::Code(code) = event.physical_key {
797                    if let Some(app) = &mut self.app {
798                        if let Some(mut input) = app.world.get_resource_mut::<InputState>() {
799                            if let Some(key) = KeyCode::from_winit(code) {
800                                if event.state.is_pressed() {
801                                    input.press_key(key);
802                                } else {
803                                    input.release_key(key);
804                                }
805                            }
806                        }
807                    }
808                }
809            }
810
811            WindowEvent::MouseInput { state, button, .. } => {
812                if let Some(app) = &mut self.app {
813                    if let Some(mut input) = app.world.get_resource_mut::<InputState>() {
814                        if let Some(btn) = MouseButton::from_winit(button) {
815                            if state.is_pressed() {
816                                input.press_mouse(btn);
817                            } else {
818                                input.release_mouse(btn);
819                            }
820                        }
821                    }
822                }
823            }
824
825            WindowEvent::CursorMoved { position, .. } => {
826                if let Some(app) = &mut self.app {
827                    if let Some(mut input) = app.world.get_resource_mut::<InputState>() {
828                        input.set_mouse_position(glam::Vec2::new(position.x as f32, position.y as f32));
829                    }
830                }
831            }
832
833            WindowEvent::MouseWheel { delta, .. } => {
834                if let Some(app) = &mut self.app {
835                    if let Some(mut input) = app.world.get_resource_mut::<InputState>() {
836                        let scroll = match delta {
837                            winit::event::MouseScrollDelta::LineDelta(_, y) => y,
838                            winit::event::MouseScrollDelta::PixelDelta(pos) => pos.y as f32 / 120.0,
839                        };
840                        input.add_scroll_delta(scroll);
841                    }
842                }
843            }
844
845            WindowEvent::Focused(focused) => {
846                debug!("窗口焦点变化: {}", focused);
847                self.window_state.set_focused(focused);
848            }
849
850            WindowEvent::Occluded(occluded) => {
851                debug!("窗口遮挡状态: {}", occluded);
852                self.window_state.set_minimized(occluded);
853            }
854
855            WindowEvent::RedrawRequested => {
856                self.render();
857            }
858
859            _ => {}
860        }
861    }
862
863    /// 设备事件处理
864    fn device_event(
865        &mut self,
866        _event_loop: &ActiveEventLoop,
867        _device_id: DeviceId,
868        _event: DeviceEvent,
869    ) {
870        // 后续可以添加输入处理
871    }
872
873    /// 即将等待事件
874    fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
875        // 如果持有 ECS App,每帧调用 update() 运行 ECS 系统
876        if let Some(app) = &mut self.app {
877            // 计算真实帧时间并写入 ECS DeltaTime 资源
878            let now = Instant::now();
879            let raw_dt = now.duration_since(self.last_frame_time).as_secs_f32();
880            self.last_frame_time = now;
881            // Clamp dt to [0.001, 0.1] to prevent physics explosions
882            let dt = raw_dt.clamp(0.001, 0.1);
883            app.world.insert_resource(DeltaTime(dt));
884
885            app.update();
886
887            // 帧结束,清除 just_pressed / just_released 状态
888            if let Some(mut input) = app.world.get_resource_mut::<InputState>() {
889                input.end_frame();
890            }
891        }
892
893        if let Some(window) = &self.window {
894            window.request_redraw();
895        }
896    }
897}
898
899#[cfg(test)]
900mod tests {
901    use super::*;
902
903    #[test]
904    fn test_render_app_creation() {
905        let config = WindowConfig::new().with_title("Test App");
906        let app = RenderApp::new(config);
907
908        assert_eq!(app.config().title, "Test App");
909        assert!(app.window().is_none());
910        assert!(!app.is_exit_requested());
911    }
912
913    #[test]
914    fn test_exit_request() {
915        let mut app = RenderApp::new(WindowConfig::default());
916
917        assert!(!app.is_exit_requested());
918        app.request_exit();
919        assert!(app.is_exit_requested());
920    }
921
922    #[test]
923    fn test_window_state_updates() {
924        let mut app = RenderApp::new(WindowConfig::default());
925
926        let new_size = PhysicalSize::new(1920, 1080);
927        app.handle_resize(new_size);
928        assert_eq!(app.window_state().size(), (1920, 1080));
929
930        app.handle_scale_factor_changed(2.0);
931        assert_eq!(app.window_state().scale_factor(), 2.0);
932    }
933
934    #[test]
935    fn test_render_app_config() {
936        let config = WindowConfig::new()
937            .with_title("Test")
938            .with_size(640, 480);
939        let app = RenderApp::new(config);
940
941        assert_eq!(app.config().title, "Test");
942        assert_eq!(app.config().width, 640);
943    }
944
945    #[test]
946    fn test_render_app_exit_request_toggle() {
947        let mut app = RenderApp::new(WindowConfig::new());
948        assert!(!app.is_exit_requested());
949
950        app.request_exit();
951        assert!(app.is_exit_requested());
952    }
953
954    #[test]
955    fn test_render_app_window_state() {
956        let config = WindowConfig::new().with_size(1024, 768);
957        let app = RenderApp::new(config);
958
959        // WindowState defaults to (1280, 720) since it's created via WindowState::new()
960        let state = app.window_state();
961        assert_eq!(state.size(), (1280, 720));
962    }
963}