1use crate::render_lane::ShaderComplexity;
48use khora_data::render::RenderWorld;
49
50use khora_core::renderer::api::{
51 command::BindGroupLayoutId,
52 util::{
53 dynamic_uniform_buffer::DynamicUniformRingBuffer, uniform_ring_buffer::UniformRingBuffer,
54 },
55};
56use khora_core::{
57 asset::Material,
58 renderer::{
59 api::{
60 command::{
61 BindGroupId, ComputePassDescriptor, ComputePipelineId, LoadOp, Operations,
62 RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor,
63 StoreOp,
64 },
65 core::RenderContext,
66 pipeline::enums::PrimitiveTopology,
67 pipeline::{
68 ComputePipelineSpec, LayoutKey, LayoutSpec, PipelineSpec, RenderPipelineId,
69 ShaderVariantKey,
70 },
71 resource::{BufferId, CameraUniformData},
72 scene::GpuMesh,
73 },
74 traits::CommandEncoder,
75 ForwardPlusTileConfig,
76 },
77};
78use khora_data::assets::Assets;
79use std::sync::RwLock;
80
81const TRIANGLE_COST: f32 = 0.001;
85
86const DRAW_CALL_COST: f32 = 0.1;
88
89const COMPUTE_PASS_OVERHEAD: f32 = 0.5;
91
92const PER_TILE_COST: f32 = 0.0001;
94
95const LIGHT_TILE_TEST_COST: f32 = 0.00001;
97
98mod g3 {
104 pub const LIGHTS: u32 = 0;
106 pub const LIGHT_INDICES: u32 = 4;
108 pub const LIGHT_GRID: u32 = 5;
110 pub const TILE_INFO: u32 = 6;
112 pub const SHADOW_VIEW_PROJS: u32 = 7;
114}
115
116#[derive(Debug, Default)]
123pub struct ForwardPlusGpuResources {
124 pub light_buffer: Option<BufferId>,
126 pub light_index_buffer: Option<BufferId>,
128 pub light_grid_buffer: Option<BufferId>,
130 pub tile_info_buffer: Option<BufferId>,
132 pub culling_uniforms_buffer: Option<BufferId>,
134 pub shadow_view_projs_buffer: Option<BufferId>,
137
138 pub camera_layout: Option<BindGroupLayoutId>,
140 pub model_layout: Option<BindGroupLayoutId>,
142 pub material_layout: Option<BindGroupLayoutId>,
144 pub lighting_layout: Option<BindGroupLayoutId>,
147 pub culling_layout: Option<BindGroupLayoutId>,
149
150 pub camera_ring: Option<UniformRingBuffer>,
152 pub model_ring: Option<DynamicUniformRingBuffer>,
154
155 pub culling_bind_group: Option<BindGroupId>,
157 pub culling_pipeline: Option<ComputePipelineId>,
159 pub render_pipeline: Option<RenderPipelineId>,
161}
162
163impl ForwardPlusGpuResources {
164 pub fn is_initialized(&self) -> bool {
166 self.light_buffer.is_some()
167 && self.light_index_buffer.is_some()
168 && self.light_grid_buffer.is_some()
169 && self.culling_uniforms_buffer.is_some()
170 && self.culling_bind_group.is_some()
171 && self.culling_pipeline.is_some()
172 }
173}
174
175pub struct ForwardPlusLane {
188 pub tile_config: ForwardPlusTileConfig,
190
191 pub shader_complexity: ShaderComplexity,
193
194 screen_size: (u32, u32),
196
197 pub gpu_resources: std::sync::Mutex<ForwardPlusGpuResources>,
199
200 pipeline_system:
203 std::sync::OnceLock<std::sync::Arc<dyn khora_core::renderer::traits::PipelineSystem>>,
204}
205
206impl Default for ForwardPlusLane {
207 fn default() -> Self {
208 Self {
209 tile_config: ForwardPlusTileConfig::default(),
210 shader_complexity: ShaderComplexity::SimpleLit,
211 screen_size: (1920, 1080),
212 gpu_resources: std::sync::Mutex::new(ForwardPlusGpuResources::default()),
213 pipeline_system: std::sync::OnceLock::new(),
214 }
215 }
216}
217
218impl ForwardPlusLane {
219 pub fn new() -> Self {
226 Self::default()
227 }
228
229 pub fn with_config(config: ForwardPlusTileConfig) -> Self {
235 Self {
236 tile_config: config,
237 ..Default::default()
238 }
239 }
240
241 pub fn with_complexity(complexity: ShaderComplexity) -> Self {
243 Self {
244 shader_complexity: complexity,
245 ..Default::default()
246 }
247 }
248
249 pub fn set_screen_size(&mut self, width: u32, height: u32) {
254 self.screen_size = (width, height);
255 }
256
257 pub fn tile_count(&self) -> (u32, u32) {
259 self.tile_config
260 .tile_dimensions(self.screen_size.0, self.screen_size.1)
261 }
262
263 pub fn total_tiles(&self) -> u32 {
265 let (tiles_x, tiles_y) = self.tile_count();
266 tiles_x * tiles_y
267 }
268
269 pub fn effective_light_count(&self, render_world: &RenderWorld) -> usize {
274 render_world.directional_light_count()
275 + render_world.point_light_count()
276 + render_world.spot_light_count()
277 }
278
279 fn compute_pass_cost(&self, render_world: &RenderWorld) -> f32 {
281 let total_tiles = self.total_tiles() as f32;
282 let light_count = self.effective_light_count(render_world) as f32;
283
284 COMPUTE_PASS_OVERHEAD
286 + (total_tiles * PER_TILE_COST)
287 + (total_tiles * light_count * LIGHT_TILE_TEST_COST)
288 }
289
290 fn fragment_light_factor(&self, render_world: &RenderWorld) -> f32 {
296 let total_lights = self.effective_light_count(render_world) as f32;
297
298 if total_lights == 0.0 {
299 return 1.0;
300 }
301
302 let effective_lights = total_lights
305 .sqrt()
306 .min(self.tile_config.max_lights_per_tile as f32);
307
308 1.0 + (effective_lights * 0.02)
309 }
310}
311
312impl khora_core::lane::Lane for ForwardPlusLane {
313 fn strategy_name(&self) -> &'static str {
314 "ForwardPlus"
315 }
316
317 fn lane_kind(&self) -> khora_core::lane::LaneKind {
318 khora_core::lane::LaneKind::Render
319 }
320
321 fn estimate_cost(&self, ctx: &khora_core::lane::LaneContext) -> f32 {
322 let render_world = match ctx.get::<khora_core::lane::Ref<khora_data::render::RenderWorld>>()
323 {
324 Some(slot) => slot.get(),
325 None => return 1.0,
326 };
327 let gpu_meshes = match ctx.get::<std::sync::Arc<
328 std::sync::RwLock<
329 khora_data::assets::Assets<khora_core::renderer::api::scene::GpuMesh>,
330 >,
331 >>() {
332 Some(arc) => arc,
333 None => return 1.0,
334 };
335 self.estimate_render_cost(render_world, gpu_meshes)
336 }
337
338 fn on_initialize(
339 &self,
340 ctx: &mut khora_core::lane::LaneContext,
341 ) -> Result<(), khora_core::lane::LaneError> {
342 let device = ctx
343 .get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
344 .ok_or(khora_core::lane::LaneError::missing(
345 "Arc<dyn GraphicsDevice>",
346 ))?
347 .clone();
348 let pipeline_system = ctx
349 .get::<std::sync::Arc<dyn khora_core::renderer::traits::PipelineSystem>>()
350 .ok_or(khora_core::lane::LaneError::missing(
351 "Arc<dyn PipelineSystem>",
352 ))?
353 .clone();
354 let _ = self.pipeline_system.set(pipeline_system.clone());
357 self.on_gpu_init(device.as_ref(), pipeline_system.as_ref())
358 .map_err(|e| khora_core::lane::LaneError::InitializationFailed(Box::new(e)))
359 }
360
361 fn execute(
362 &self,
363 ctx: &mut khora_core::lane::LaneContext,
364 ) -> Result<(), khora_core::lane::LaneError> {
365 use khora_core::lane::{LaneError, Ref, Slot};
366 let device = ctx
367 .get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
368 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
369 .clone();
370 let gpu_meshes = ctx
371 .get::<std::sync::Arc<
372 std::sync::RwLock<
373 khora_data::assets::Assets<khora_core::renderer::api::scene::GpuMesh>,
374 >,
375 >>()
376 .ok_or(LaneError::missing("Arc<RwLock<Assets<GpuMesh>>>"))?
377 .clone();
378 let encoder = ctx
379 .get::<Slot<dyn khora_core::renderer::traits::CommandEncoder>>()
380 .ok_or(LaneError::missing("Slot<dyn CommandEncoder>"))?
381 .get();
382 let render_world = ctx
383 .get::<Ref<khora_data::render::RenderWorld>>()
384 .ok_or(LaneError::missing("Ref<RenderWorld>"))?
385 .get();
386 let color_target = ctx
387 .get::<khora_core::lane::ColorTarget>()
388 .ok_or(LaneError::missing("ColorTarget"))?
389 .0;
390 let depth_target = ctx
391 .get::<khora_core::lane::DepthTarget>()
392 .ok_or(LaneError::missing("DepthTarget"))?
393 .0;
394 let clear_color = ctx
395 .get::<khora_core::lane::ClearColor>()
396 .ok_or(LaneError::missing("ClearColor"))?
397 .0;
398
399 let render_ctx = khora_core::renderer::api::core::RenderContext::new(
400 &color_target,
401 Some(&depth_target),
402 clear_color,
403 );
404
405 let (shadow_entries, shadow_bindings) = ctx
409 .get::<Slot<khora_core::lane::OutputDeck>>()
410 .map(|s| {
411 let frame = s
412 .get()
413 .slot::<khora_core::renderer::api::shadow::ShadowFrame>();
414 (frame.entries.clone(), frame.bindings)
415 })
416 .unwrap_or_default();
417
418 let ibl_bindings = ctx
419 .get::<khora_core::renderer::api::ibl::IblGpuBindings>()
420 .copied();
421
422 self.render(
423 render_world,
424 &shadow_entries,
425 shadow_bindings,
426 ibl_bindings,
427 device.as_ref(),
428 encoder,
429 &render_ctx,
430 &gpu_meshes,
431 );
432 Ok(())
433 }
434
435 fn on_shutdown(&self, ctx: &mut khora_core::lane::LaneContext) {
436 if let Some(device) = ctx.get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
437 {
438 self.on_gpu_shutdown(device.as_ref());
439 }
440 }
441
442 fn as_any(&self) -> &dyn std::any::Any {
443 self
444 }
445
446 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
447 self
448 }
449}
450
451impl ForwardPlusLane {
452 pub fn get_pipeline_for_material(
454 &self,
455 _material: Option<&khora_core::asset::AssetHandle<Box<dyn Material>>>,
456 ) -> RenderPipelineId {
457 let resources = crate::lock_or_log!(
461 self.gpu_resources.lock(),
462 "ForwardPlusLane::get_pipeline_for_material",
463 RenderPipelineId(0)
464 );
465 resources.render_pipeline.unwrap_or(RenderPipelineId(0))
466 }
467
468 #[allow(clippy::too_many_arguments)]
469 fn render(
470 &self,
471 render_world: &RenderWorld,
472 shadow_entries: &khora_data::render::ShadowEntries,
473 shadow_bindings: Option<khora_data::render::ShadowGpuBindings>,
474 ibl_bindings: Option<khora_core::renderer::api::ibl::IblGpuBindings>,
475 device: &dyn khora_core::renderer::GraphicsDevice,
476 encoder: &mut dyn CommandEncoder,
477 render_ctx: &RenderContext,
478 gpu_meshes: &RwLock<Assets<GpuMesh>>,
479 ) {
480 let mut resources =
481 crate::lock_or_log!(self.gpu_resources.lock(), "ForwardPlusLane::render");
482
483 let Some(view) = render_world.views.first() else {
488 let color_attachment = khora_core::renderer::api::command::RenderPassColorAttachment {
489 view: render_ctx.color_target,
490 resolve_target: None,
491 ops: khora_core::renderer::api::command::Operations {
492 load: khora_core::renderer::api::command::LoadOp::Clear(render_ctx.clear_color),
493 store: khora_core::renderer::api::command::StoreOp::Store,
494 },
495 base_array_layer: 0,
496 base_mip_level: 0,
497 };
498 let clear_desc = khora_core::renderer::api::command::RenderPassDescriptor {
499 label: Some("ForwardPlus Clear-Only Pass"),
500 color_attachments: &[color_attachment],
501 depth_stencil_attachment: render_ctx.depth_target.map(|depth_view| {
502 khora_core::renderer::api::command::RenderPassDepthStencilAttachment {
503 view: depth_view,
504 depth_ops: Some(khora_core::renderer::api::command::Operations {
505 load: khora_core::renderer::api::command::LoadOp::Clear(1.0),
506 store: khora_core::renderer::api::command::StoreOp::Store,
507 }),
508 stencil_ops: None,
509 base_array_layer: 0,
510 }
511 }),
512 };
513 let _ = encoder.begin_render_pass(&clear_desc);
514 return;
515 };
516
517 let camera_uniforms = CameraUniformData {
519 view_projection: view.view_proj.to_cols_array_2d(),
520 camera_position: [view.position.x, view.position.y, view.position.z, 1.0],
521 };
522
523 let camera_bind_group = if let Some(ref mut ring) = resources.camera_ring {
524 ring.advance();
525 if let Err(e) = ring.write(device, bytemuck::bytes_of(&camera_uniforms)) {
526 log::error!("Failed to write camera ring buffer: {:?}", e);
527 return;
528 }
529 *ring.current_bind_group()
530 } else {
531 return;
532 };
533
534 if let Some(tile_buffer) = resources.tile_info_buffer {
536 let config = self.tile_config;
537 let (width, height) = self.screen_size;
538 let num_tiles_x = width.div_ceil(config.tile_size.pixels());
539 let num_tiles_y = height.div_ceil(config.tile_size.pixels());
540 let tile_info = [
541 num_tiles_x,
542 num_tiles_y,
543 config.tile_size.pixels(),
544 config.max_lights_per_tile,
545 ];
546 if let Err(e) = device.write_buffer(tile_buffer, 0, bytemuck::cast_slice(&tile_info)) {
547 log::error!("ForwardPlusLane::render: tile info buffer write failed: {e:?}");
548 }
549 }
550
551 use khora_data::render::ShadowEntry;
561 let lights: Vec<_> = render_world
562 .lights
563 .iter()
564 .enumerate()
565 .map(|(i, l)| {
566 let mut gl = khora_core::renderer::GpuLight::from_parts(
567 [l.position.x, l.position.y, l.position.z],
568 [l.direction.x, l.direction.y, l.direction.z],
569 &l.light_type,
570 );
571 match shadow_entries.get(i) {
572 Some(ShadowEntry::Atlas2D { atlas_index, .. }) => {
573 gl.shadow_map_index = *atlas_index;
574 }
575 Some(ShadowEntry::Cube {
576 cube_array_index,
577 far_plane,
578 ..
579 }) => {
580 gl.shadow_map_index = *cube_array_index;
581 gl.shadow_far_plane = *far_plane;
582 }
583 None => {}
584 }
585 gl
586 })
587 .collect();
588
589 if let Some(light_buffer) = resources.light_buffer {
590 if let Err(e) = device.write_buffer(light_buffer, 0, bytemuck::cast_slice(&lights)) {
591 log::error!("ForwardPlusLane::render: light data buffer write failed: {e:?}");
592 }
593 }
594
595 let shadow_view_projs: Vec<[[f32; 4]; 4]> = render_world
600 .lights
601 .iter()
602 .enumerate()
603 .map(|(i, _)| match shadow_entries.get(i) {
604 Some(ShadowEntry::Atlas2D { view_proj, .. }) => view_proj.to_cols_array_2d(),
605 _ => khora_core::math::Mat4::IDENTITY.to_cols_array_2d(),
606 })
607 .collect();
608
609 if let Some(svp_buffer) = resources.shadow_view_projs_buffer {
610 if !shadow_view_projs.is_empty() {
611 if let Err(e) =
612 device.write_buffer(svp_buffer, 0, bytemuck::cast_slice(&shadow_view_projs))
613 {
614 log::error!(
615 "ForwardPlusLane::render: shadow view-proj buffer write failed: {e:?}"
616 );
617 }
618 }
619 }
620
621 if let Some(culling_buffer) = resources.culling_uniforms_buffer {
623 let config = self.tile_config;
624 let (width, height) = self.screen_size;
625 let num_tiles_x = width.div_ceil(config.tile_size.pixels());
626 let num_tiles_y = height.div_ceil(config.tile_size.pixels());
627
628 let inv_vp = view.view_proj.inverse().unwrap_or_default();
629
630 let culling_data = khora_core::renderer::api::scene::CullingUniformsData {
631 view_projection: view.view_proj.to_cols_array_2d(),
632 inverse_projection: inv_vp.to_cols_array_2d(),
633 screen_dimensions: [width as f32, height as f32],
634 tile_count: [num_tiles_x, num_tiles_y],
635 num_lights: lights.len() as u32,
636 tile_size: config.tile_size.pixels(),
637 _padding: [0.0; 2],
638 };
639
640 if let Err(e) =
641 device.write_buffer(culling_buffer, 0, bytemuck::bytes_of(&culling_data))
642 {
643 log::error!("ForwardPlusLane::render: culling uniforms buffer write failed: {e:?}");
644 }
645 }
646
647 if let (Some(culling_pipeline), Some(culling_bg)) =
649 (resources.culling_pipeline, resources.culling_bind_group)
650 {
651 let mut compute_pass = encoder.begin_compute_pass(&ComputePassDescriptor {
652 label: Some("Forward+ Light Culling Pass"),
653 timestamp_writes: None,
654 });
655 compute_pass.set_pipeline(&culling_pipeline);
656 compute_pass.set_bind_group(0, &culling_bg, &[]);
657
658 let config = self.tile_config;
659 let (width, height) = self.screen_size;
660 let num_tiles_x = width.div_ceil(config.tile_size.pixels());
661 let num_tiles_y = height.div_ceil(config.tile_size.pixels());
662 compute_pass.dispatch_workgroups(num_tiles_x, num_tiles_y, 1);
663 }
664
665 let mut draw_commands = Vec::new();
668 let mut transparent_draws: Vec<(f32, khora_core::renderer::api::command::DrawCommand)> =
669 Vec::new();
670
671 if let Some(ref mut ring) = resources.model_ring {
672 ring.advance();
673 }
674
675 let fallback_pipeline = resources.render_pipeline.unwrap_or(RenderPipelineId(0));
678
679 let gpu_mesh_assets = crate::lock_or_log!(gpu_meshes.read(), "ForwardPlusLane::render");
680 for extracted_mesh in &render_world.meshes {
681 if let Some(gpu_mesh_handle) = gpu_mesh_assets.get(&extracted_mesh.cpu_mesh_uuid) {
682 let Some(gpu_material) = &extracted_mesh.gpu_material else {
685 continue;
686 };
687
688 let pipeline_id = self
691 .pipeline_system
692 .get()
693 .map(|ps| {
694 ps.pipeline(
695 device,
696 &render_pipeline_spec(
697 device,
698 gpu_material.variant.clone(),
699 gpu_material.double_sided,
700 gpu_material.blend,
701 ),
702 )
703 })
704 .transpose()
705 .unwrap_or_else(|e| {
706 log::error!("ForwardPlusLane: pipeline resolve failed: {:?}", e);
707 None
708 })
709 .unwrap_or(fallback_pipeline);
710
711 let model_mat = extracted_mesh.transform.to_matrix();
713 let normal_mat = model_mat.inverse().unwrap_or_default().transpose();
714
715 let model_uniforms = khora_core::renderer::api::scene::ModelUniforms {
716 model_matrix: model_mat.to_cols_array_2d(),
717 normal_matrix: normal_mat.to_cols_array_2d(),
718 };
719
720 let (model_bg, model_offset) = if let Some(ref mut ring) = resources.model_ring {
722 let offset = match ring.push(device, bytemuck::bytes_of(&model_uniforms)) {
723 Ok(off) => off,
724 Err(_) => continue,
725 };
726 (*ring.current_bind_group(), offset)
727 } else {
728 continue;
729 };
730
731 let command = khora_core::renderer::api::command::DrawCommand {
732 pipeline: pipeline_id,
733 vertex_buffer: gpu_mesh_handle.vertex_buffer,
734 index_buffer: gpu_mesh_handle.index_buffer,
735 index_count: gpu_mesh_handle.index_count,
736 index_format: gpu_mesh_handle.index_format,
737 model_bind_group: Some(model_bg),
738 model_offset,
739 material_bind_group: Some(gpu_material.bind_group),
740 material_offset: 0,
741 };
742 if gpu_material.blend {
743 transparent_draws.push((
744 crate::render_lane::camera_distance_sq(&model_mat, view.position),
745 command,
746 ));
747 } else {
748 draw_commands.push(command);
749 }
750 }
751 }
752
753 draw_commands.sort_by_key(|cmd| cmd.pipeline.0);
755 transparent_draws
758 .sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
759
760 let color_attachment = RenderPassColorAttachment {
762 view: render_ctx.color_target,
763 resolve_target: None,
764 ops: Operations {
765 load: LoadOp::Clear(render_ctx.clear_color),
766 store: StoreOp::Store,
767 },
768 base_array_layer: 0,
769 base_mip_level: 0,
770 };
771
772 let render_pass_desc = RenderPassDescriptor {
773 label: Some("ForwardPlus Render Pass"),
774 color_attachments: &[color_attachment],
775 depth_stencil_attachment: render_ctx.depth_target.map(|depth_view| {
776 RenderPassDepthStencilAttachment {
777 view: depth_view,
778 depth_ops: Some(Operations {
779 load: LoadOp::Clear(1.0),
780 store: StoreOp::Store,
781 }),
782 stencil_ops: None,
783 base_array_layer: 0,
784 }
785 }),
786 };
787
788 let Some(shadow_bindings) = shadow_bindings else {
799 log::warn!(
800 "ForwardPlusLane: ShadowGpuBindings not available (shadow agent inactive?), skipping render"
801 );
802 return;
803 };
804 let (
805 Some(lighting_layout),
806 Some(light_buffer),
807 Some(light_index_buffer),
808 Some(light_grid_buffer),
809 Some(tile_info_buffer),
810 Some(shadow_view_projs_buffer),
811 ) = (
812 resources.lighting_layout,
813 resources.light_buffer,
814 resources.light_index_buffer,
815 resources.light_grid_buffer,
816 resources.tile_info_buffer,
817 resources.shadow_view_projs_buffer,
818 )
819 else {
820 log::warn!("ForwardPlusLane: lighting GPU resources not initialized, skipping render");
821 return;
822 };
823
824 use khora_core::renderer::api::command::{BindGroupDescriptor, BindGroupEntry};
825 let mut lighting_entries: Vec<BindGroupEntry> = Vec::with_capacity(8);
826 lighting_entries.push(BindGroupEntry::buffer(g3::LIGHTS, light_buffer, 0, None));
827 khora_data::render::shadow_bindings::fill_shadow_bind_group_entries(
829 &shadow_bindings,
830 &mut lighting_entries,
831 );
832 lighting_entries.push(BindGroupEntry::buffer(
833 g3::LIGHT_INDICES,
834 light_index_buffer,
835 0,
836 None,
837 ));
838 lighting_entries.push(BindGroupEntry::buffer(
839 g3::LIGHT_GRID,
840 light_grid_buffer,
841 0,
842 None,
843 ));
844 lighting_entries.push(BindGroupEntry::buffer(
845 g3::TILE_INFO,
846 tile_info_buffer,
847 0,
848 None,
849 ));
850 lighting_entries.push(BindGroupEntry::buffer(
851 g3::SHADOW_VIEW_PROJS,
852 shadow_view_projs_buffer,
853 0,
854 None,
855 ));
856 let Some(ibl) = ibl_bindings else {
859 log::warn!("ForwardPlusLane: IBL bindings not available, skipping render");
860 return;
861 };
862 khora_core::renderer::api::ibl::fill_ibl_bind_group_entries(&ibl, 8, &mut lighting_entries);
863 let lighting_bg = match device.create_bind_group(&BindGroupDescriptor {
864 label: Some("forward_plus_lighting_bind_group"),
865 layout: lighting_layout,
866 entries: &lighting_entries,
867 }) {
868 Ok(bg) => bg,
869 Err(e) => {
870 log::error!(
871 "ForwardPlusLane: failed to create lighting bind group: {:?}",
872 e
873 );
874 return;
875 }
876 };
877
878 let mut render_pass = encoder.begin_render_pass(&render_pass_desc);
879
880 render_pass.set_bind_group(0, &camera_bind_group, &[]);
882
883 render_pass.set_bind_group(3, &lighting_bg, &[]);
885
886 let mut current_pipeline: Option<RenderPipelineId> = None;
891 for cmd in draw_commands
892 .iter()
893 .chain(transparent_draws.iter().map(|(_, cmd)| cmd))
894 {
895 if current_pipeline != Some(cmd.pipeline) {
896 render_pass.set_pipeline(&cmd.pipeline);
897 current_pipeline = Some(cmd.pipeline);
898 }
899 if let Some(ref bg) = cmd.model_bind_group {
900 render_pass.set_bind_group(1, bg, &[cmd.model_offset]);
901 }
902 if let Some(ref bg) = cmd.material_bind_group {
903 render_pass.set_bind_group(2, bg, &[]);
904 }
905
906 render_pass.set_vertex_buffer(0, &cmd.vertex_buffer, 0);
907 render_pass.set_index_buffer(&cmd.index_buffer, 0, cmd.index_format);
908 render_pass.draw_indexed(0..cmd.index_count, 0, 0..1);
909 }
910
911 drop(render_pass);
912 let _ = device.destroy_bind_group(lighting_bg);
913 }
914
915 fn estimate_render_cost(
916 &self,
917 render_world: &RenderWorld,
918 gpu_meshes: &RwLock<Assets<GpuMesh>>,
919 ) -> f32 {
920 let gpu_mesh_assets = crate::lock_or_log!(
921 gpu_meshes.read(),
922 "ForwardPlusLane::estimate_render_cost",
923 0.0
924 );
925
926 let mut total_triangles = 0u32;
927 let mut draw_call_count = 0u32;
928
929 for extracted_mesh in &render_world.meshes {
930 if let Some(gpu_mesh) = gpu_mesh_assets.get(&extracted_mesh.cpu_mesh_uuid) {
931 let triangle_count = match gpu_mesh.primitive_topology {
932 PrimitiveTopology::TriangleList => gpu_mesh.index_count / 3,
933 PrimitiveTopology::TriangleStrip => {
934 if gpu_mesh.index_count >= 3 {
935 gpu_mesh.index_count - 2
936 } else {
937 0
938 }
939 }
940 PrimitiveTopology::LineList
941 | PrimitiveTopology::LineStrip
942 | PrimitiveTopology::PointList => 0,
943 };
944
945 total_triangles += triangle_count;
946 draw_call_count += 1;
947 }
948 }
949
950 let geometry_cost =
952 (total_triangles as f32 * TRIANGLE_COST) + (draw_call_count as f32 * DRAW_CALL_COST);
953
954 let shader_multiplier = self.shader_complexity.cost_multiplier();
956
957 let compute_cost = self.compute_pass_cost(render_world);
959
960 let light_factor = self.fragment_light_factor(render_world);
962
963 compute_cost + (geometry_cost * shader_multiplier * light_factor)
965 }
966
967 fn on_gpu_init(
968 &self,
969 device: &dyn khora_core::renderer::GraphicsDevice,
970 pipeline_system: &dyn khora_core::renderer::traits::PipelineSystem,
971 ) -> Result<(), khora_core::renderer::error::RenderError> {
972 use khora_core::renderer::api::{
973 command::{BindGroupDescriptor, BindGroupEntry},
974 resource::CameraUniformData,
975 scene::ModelUniforms,
976 };
977 use std::borrow::Cow;
978
979 log::info!("ForwardPlusLane: Initializing GPU resources...");
980
981 let variant = ShaderVariantKey::empty();
986 let camera_layout = pipeline_system.layout(device, LayoutKey::Camera, &variant)?;
987 let material_layout = pipeline_system.layout(device, LayoutKey::Material, &variant)?;
988 let model_layout = pipeline_system.inline_layout(
989 device,
990 FP_MODEL_LAYOUT_LABEL,
991 &fp_model_layout_entries(),
992 )?;
993 let lighting_layout = pipeline_system.inline_layout(
994 device,
995 FP_LIGHTING_LAYOUT_LABEL,
996 &fp_lighting_layout_entries(),
997 )?;
998 let culling_layout = pipeline_system.inline_layout(
999 device,
1000 FP_CULLING_LAYOUT_LABEL,
1001 &fp_culling_layout_entries(),
1002 )?;
1003
1004 let pipeline_id = pipeline_system.pipeline(
1008 device,
1009 &render_pipeline_spec(device, ShaderVariantKey::empty(), false, false),
1010 )?;
1011 let culling_pipeline =
1012 pipeline_system.compute_pipeline(device, &culling_pipeline_spec())?;
1013
1014 let light_buffer = device
1018 .create_buffer(&khora_core::renderer::api::resource::BufferDescriptor {
1019 label: Some(Cow::Borrowed("Forward+ Light Buffer")),
1020 size: 64 * 1024,
1021 usage: khora_core::renderer::api::resource::BufferUsage::STORAGE
1022 | khora_core::renderer::api::resource::BufferUsage::COPY_DST,
1023 mapped_at_creation: false,
1024 })
1025 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
1026
1027 let light_index_buffer = device
1029 .create_buffer(&khora_core::renderer::api::resource::BufferDescriptor {
1030 label: Some(Cow::Borrowed("Forward+ Light Index Buffer")),
1031 size: 120 * 68 * 256 * 4,
1032 usage: khora_core::renderer::api::resource::BufferUsage::STORAGE
1033 | khora_core::renderer::api::resource::BufferUsage::COPY_DST,
1034 mapped_at_creation: false,
1035 })
1036 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
1037
1038 let light_grid_buffer = device
1040 .create_buffer(&khora_core::renderer::api::resource::BufferDescriptor {
1041 label: Some(Cow::Borrowed("Forward+ Light Grid Buffer")),
1042 size: 120 * 68 * 2 * 4,
1043 usage: khora_core::renderer::api::resource::BufferUsage::STORAGE
1044 | khora_core::renderer::api::resource::BufferUsage::COPY_DST,
1045 mapped_at_creation: false,
1046 })
1047 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
1048
1049 let tile_info_buffer = device
1051 .create_buffer(&khora_core::renderer::api::resource::BufferDescriptor {
1052 label: Some(Cow::Borrowed("Forward+ Tile Info")),
1053 size: 256,
1054 usage: khora_core::renderer::api::resource::BufferUsage::UNIFORM
1055 | khora_core::renderer::api::resource::BufferUsage::COPY_DST,
1056 mapped_at_creation: false,
1057 })
1058 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
1059
1060 let shadow_view_projs_buffer = device
1064 .create_buffer(&khora_core::renderer::api::resource::BufferDescriptor {
1065 label: Some(Cow::Borrowed("Forward+ Shadow ViewProj Buffer")),
1066 size: 64 * 1024,
1067 usage: khora_core::renderer::api::resource::BufferUsage::STORAGE
1068 | khora_core::renderer::api::resource::BufferUsage::COPY_DST,
1069 mapped_at_creation: false,
1070 })
1071 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
1072
1073 let culling_uniforms_buffer = device
1075 .create_buffer(&khora_core::renderer::api::resource::BufferDescriptor {
1076 label: Some(Cow::Borrowed("Forward+ Culling Uniforms")),
1077 size: 256,
1078 usage: khora_core::renderer::api::resource::BufferUsage::UNIFORM
1079 | khora_core::renderer::api::resource::BufferUsage::COPY_DST,
1080 mapped_at_creation: false,
1081 })
1082 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
1083
1084 let camera_ring = UniformRingBuffer::new(
1086 device,
1087 camera_layout,
1088 0,
1089 std::mem::size_of::<CameraUniformData>() as u64,
1090 "Forward+ Camera Ring",
1091 )
1092 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
1093
1094 let model_ring = DynamicUniformRingBuffer::new(
1095 device,
1096 model_layout,
1097 0,
1098 std::mem::size_of::<ModelUniforms>() as u32,
1099 khora_core::renderer::api::util::dynamic_uniform_buffer::DEFAULT_MAX_ELEMENTS,
1100 khora_core::renderer::api::util::dynamic_uniform_buffer::MIN_UNIFORM_ALIGNMENT,
1101 "Forward+ Model Ring",
1102 )
1103 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
1104
1105 let culling_bg = device
1112 .create_bind_group(&BindGroupDescriptor {
1113 label: Some("Forward+ Culling Bind Group"),
1114 layout: culling_layout,
1115 entries: &[
1116 BindGroupEntry::buffer(0, culling_uniforms_buffer, 0, None),
1117 BindGroupEntry::buffer(1, light_buffer, 0, None),
1118 BindGroupEntry::buffer(2, light_index_buffer, 0, None),
1119 BindGroupEntry::buffer(3, light_grid_buffer, 0, None),
1120 ],
1121 })
1122 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
1123
1124 let mut res = self.gpu_resources.lock().map_err(|_| {
1130 khora_core::renderer::error::RenderError::ResourceError(
1131 khora_core::renderer::error::ResourceError::BackendError(
1132 "ForwardPlusLane gpu_resources mutex poisoned".into(),
1133 ),
1134 )
1135 })?;
1136 res.light_buffer = Some(light_buffer);
1137 res.light_index_buffer = Some(light_index_buffer);
1138 res.light_grid_buffer = Some(light_grid_buffer);
1139 res.tile_info_buffer = Some(tile_info_buffer);
1140 res.culling_uniforms_buffer = Some(culling_uniforms_buffer);
1141 res.shadow_view_projs_buffer = Some(shadow_view_projs_buffer);
1142 res.camera_layout = Some(camera_layout);
1143 res.model_layout = Some(model_layout);
1144 res.material_layout = Some(material_layout);
1145 res.lighting_layout = Some(lighting_layout);
1146 res.culling_layout = Some(culling_layout);
1147 res.camera_ring = Some(camera_ring);
1148 res.model_ring = Some(model_ring);
1149 res.culling_bind_group = Some(culling_bg);
1150 res.culling_pipeline = Some(culling_pipeline);
1151 res.render_pipeline = Some(pipeline_id);
1152
1153 Ok(())
1154 }
1155
1156 fn on_gpu_shutdown(&self, device: &dyn khora_core::renderer::GraphicsDevice) {
1157 let mut resources = crate::lock_or_log!(
1158 self.gpu_resources.lock(),
1159 "ForwardPlusLane::on_gpu_shutdown"
1160 );
1161
1162 if let Some(ring) = resources.camera_ring.take() {
1163 ring.destroy(device);
1164 }
1165 if let Some(ring) = resources.model_ring.take() {
1166 ring.destroy(device);
1167 }
1168
1169 if let Some(id) = resources.light_buffer.take() {
1170 device.destroy_buffer(id).ok();
1171 }
1172 if let Some(id) = resources.light_index_buffer.take() {
1173 device.destroy_buffer(id).ok();
1174 }
1175 if let Some(id) = resources.light_grid_buffer.take() {
1176 let _ = device.destroy_buffer(id);
1177 }
1178 if let Some(id) = resources.culling_uniforms_buffer.take() {
1179 let _ = device.destroy_buffer(id);
1180 }
1181 if let Some(id) = resources.shadow_view_projs_buffer.take() {
1182 let _ = device.destroy_buffer(id);
1183 }
1184 }
1185}
1186
1187const FP_MODEL_LAYOUT_LABEL: &str = "forward_plus_model_layout";
1191const FP_LIGHTING_LAYOUT_LABEL: &str = "forward_plus_lighting_layout";
1193const FP_CULLING_LAYOUT_LABEL: &str = "forward_plus_culling_layout";
1195
1196fn fp_model_layout_entries() -> Vec<khora_core::renderer::api::command::BindGroupLayoutEntry> {
1198 use khora_core::renderer::api::command::{
1199 BindGroupLayoutEntry, BindingType, BufferBindingType,
1200 };
1201 use khora_core::renderer::api::scene::ModelUniforms;
1202 use khora_core::renderer::api::util::ShaderStageFlags;
1203 vec![BindGroupLayoutEntry {
1204 binding: 0,
1205 visibility: ShaderStageFlags::VERTEX,
1206 ty: BindingType::Buffer {
1207 ty: BufferBindingType::Uniform,
1208 has_dynamic_offset: true,
1209 min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::<ModelUniforms>() as u64),
1210 },
1211 }]
1212}
1213
1214fn fp_lighting_layout_entries() -> Vec<khora_core::renderer::api::command::BindGroupLayoutEntry> {
1220 use khora_core::renderer::api::command::{BindGroupLayoutEntry, BufferBindingType};
1221 use khora_core::renderer::api::util::ShaderStageFlags;
1222 let mut entries: Vec<BindGroupLayoutEntry> = vec![BindGroupLayoutEntry::buffer(
1223 g3::LIGHTS,
1224 ShaderStageFlags::FRAGMENT,
1225 BufferBindingType::Storage { read_only: true },
1226 false,
1227 None,
1228 )];
1229 entries.extend(khora_data::render::shadow_bindings::shadow_bind_group_layout_entries());
1230 entries.extend([
1231 BindGroupLayoutEntry::buffer(
1232 g3::LIGHT_INDICES,
1233 ShaderStageFlags::FRAGMENT,
1234 BufferBindingType::Storage { read_only: true },
1235 false,
1236 None,
1237 ),
1238 BindGroupLayoutEntry::buffer(
1239 g3::LIGHT_GRID,
1240 ShaderStageFlags::FRAGMENT,
1241 BufferBindingType::Storage { read_only: true },
1242 false,
1243 None,
1244 ),
1245 BindGroupLayoutEntry::buffer(
1246 g3::TILE_INFO,
1247 ShaderStageFlags::FRAGMENT,
1248 BufferBindingType::Uniform,
1249 false,
1250 None,
1251 ),
1252 BindGroupLayoutEntry::buffer(
1253 g3::SHADOW_VIEW_PROJS,
1254 ShaderStageFlags::FRAGMENT,
1255 BufferBindingType::Storage { read_only: true },
1256 false,
1257 None,
1258 ),
1259 ]);
1260 entries.extend(khora_core::renderer::api::ibl::ibl_bind_group_layout_entries(8));
1262 entries
1263}
1264
1265fn fp_culling_layout_entries() -> Vec<khora_core::renderer::api::command::BindGroupLayoutEntry> {
1267 use khora_core::renderer::api::command::{BindGroupLayoutEntry, BufferBindingType};
1268 use khora_core::renderer::api::util::ShaderStageFlags;
1269 vec![
1270 BindGroupLayoutEntry::buffer(
1271 0,
1272 ShaderStageFlags::COMPUTE,
1273 BufferBindingType::Uniform,
1274 false,
1275 None,
1276 ),
1277 BindGroupLayoutEntry::buffer(
1278 1,
1279 ShaderStageFlags::COMPUTE,
1280 BufferBindingType::Storage { read_only: true },
1281 false,
1282 None,
1283 ),
1284 BindGroupLayoutEntry::buffer(
1285 2,
1286 ShaderStageFlags::COMPUTE,
1287 BufferBindingType::Storage { read_only: false },
1288 false,
1289 None,
1290 ),
1291 BindGroupLayoutEntry::buffer(
1292 3,
1293 ShaderStageFlags::COMPUTE,
1294 BufferBindingType::Storage { read_only: false },
1295 false,
1296 None,
1297 ),
1298 ]
1299}
1300
1301fn render_pipeline_spec(
1306 device: &dyn khora_core::renderer::GraphicsDevice,
1307 variant: ShaderVariantKey,
1308 double_sided: bool,
1309 blend: bool,
1310) -> PipelineSpec {
1311 use khora_core::renderer::api::pipeline::enums::{
1312 CompareFunction, CullMode, VertexFormat, VertexStepMode,
1313 };
1314 use khora_core::renderer::api::pipeline::state::{
1315 BlendStateDescriptor, ColorWrites, DepthBiasState, StencilFaceState,
1316 };
1317 use khora_core::renderer::api::pipeline::{
1318 ColorTargetStateDescriptor, DepthStencilStateDescriptor, MultisampleStateDescriptor,
1319 PrimitiveStateDescriptor, VertexAttributeDescriptor, VertexBufferLayoutDescriptor,
1320 };
1321 use khora_core::renderer::api::util::{SampleCount, TextureFormat};
1322 use std::borrow::Cow;
1323
1324 PipelineSpec {
1325 label: "ForwardPlus Pipeline",
1326 shader: "khora::pipelines::forward_plus",
1327 variant,
1328 bind_group_layouts: vec![
1329 LayoutSpec::Named(LayoutKey::Camera),
1330 LayoutSpec::Inline {
1331 label: FP_MODEL_LAYOUT_LABEL,
1332 entries: Cow::Owned(fp_model_layout_entries()),
1333 },
1334 LayoutSpec::Named(LayoutKey::Material),
1335 LayoutSpec::Inline {
1336 label: FP_LIGHTING_LAYOUT_LABEL,
1337 entries: Cow::Owned(fp_lighting_layout_entries()),
1338 },
1339 ],
1340 vertex_buffers: vec![VertexBufferLayoutDescriptor {
1341 array_stride: 32,
1342 step_mode: VertexStepMode::Vertex,
1343 attributes: Cow::Owned(vec![
1344 VertexAttributeDescriptor {
1345 format: VertexFormat::Float32x3,
1346 offset: 0,
1347 shader_location: 0,
1348 },
1349 VertexAttributeDescriptor {
1350 format: VertexFormat::Float32x3,
1351 offset: 12,
1352 shader_location: 1,
1353 },
1354 VertexAttributeDescriptor {
1355 format: VertexFormat::Float32x2,
1356 offset: 24,
1357 shader_location: 2,
1358 },
1359 ]),
1360 }],
1361 vs_entry: "vs_main",
1362 fs_entry: Some("fs_main"),
1363 primitive: PrimitiveStateDescriptor {
1364 topology: PrimitiveTopology::TriangleList,
1365 cull_mode: if double_sided {
1368 None
1369 } else {
1370 Some(CullMode::Back)
1371 },
1372 ..Default::default()
1373 },
1374 depth_stencil: Some(DepthStencilStateDescriptor {
1375 format: TextureFormat::Depth32Float,
1376 depth_write_enabled: !blend,
1379 depth_compare: CompareFunction::Less,
1380 stencil_front: StencilFaceState::default(),
1381 stencil_back: StencilFaceState::default(),
1382 stencil_read_mask: 0,
1383 stencil_write_mask: 0,
1384 bias: DepthBiasState::default(),
1385 }),
1386 color_targets: vec![ColorTargetStateDescriptor {
1387 format: device
1388 .get_surface_format()
1389 .unwrap_or(TextureFormat::Rgba8UnormSrgb),
1390 blend: blend.then(BlendStateDescriptor::alpha_blending),
1391 write_mask: ColorWrites::ALL,
1392 }],
1393 multisample: MultisampleStateDescriptor {
1394 count: SampleCount::X1,
1395 mask: !0,
1396 alpha_to_coverage_enabled: false,
1397 },
1398 }
1399}
1400
1401fn culling_pipeline_spec() -> ComputePipelineSpec {
1403 use std::borrow::Cow;
1404 ComputePipelineSpec {
1405 label: "Forward+ Culling Pipeline",
1406 shader: "khora::pipelines::light_culling",
1407 variant: ShaderVariantKey::empty(),
1408 bind_group_layouts: vec![LayoutSpec::Inline {
1409 label: FP_CULLING_LAYOUT_LABEL,
1410 entries: Cow::Owned(fp_culling_layout_entries()),
1411 }],
1412 entry_point: "cs_main",
1413 }
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418 use super::*;
1419 use khora_core::lane::Lane;
1420 use khora_core::renderer::TileSize;
1421
1422 #[test]
1423 fn test_forward_plus_lane_creation() {
1424 let lane = ForwardPlusLane::new();
1425 assert_eq!(lane.tile_config.tile_size, TileSize::X16);
1426 assert_eq!(lane.tile_config.max_lights_per_tile, 128);
1427 assert_eq!(lane.shader_complexity, ShaderComplexity::SimpleLit);
1428 }
1429
1430 #[test]
1431 fn test_forward_plus_lane_with_config() {
1432 let config = ForwardPlusTileConfig {
1433 tile_size: TileSize::X32,
1434 max_lights_per_tile: 256,
1435 use_depth_prepass: true,
1436 };
1437 let lane = ForwardPlusLane::with_config(config);
1438
1439 assert_eq!(lane.tile_config.tile_size, TileSize::X32);
1440 assert_eq!(lane.tile_config.max_lights_per_tile, 256);
1441 assert!(lane.tile_config.use_depth_prepass);
1442 }
1443
1444 #[test]
1445 fn test_tile_count_calculation() {
1446 let mut lane = ForwardPlusLane::new();
1447 lane.set_screen_size(1920, 1080);
1448
1449 let (tiles_x, tiles_y) = lane.tile_count();
1450 assert_eq!(tiles_x, 120); assert_eq!(tiles_y, 68); }
1453
1454 #[test]
1455 fn test_strategy_name() {
1456 let lane = ForwardPlusLane::new();
1457 assert_eq!(lane.strategy_name(), "ForwardPlus");
1458 }
1459
1460 #[test]
1461 fn test_pipeline_id() {
1462 let lane = ForwardPlusLane::new();
1463 let pipeline = lane.get_pipeline_for_material(None);
1465 assert_eq!(pipeline, RenderPipelineId(0));
1466 }
1467}