1use khora_core::renderer::api::command::BindGroupLayoutId;
28
29use khora_core::renderer::api::pipeline::{LayoutKey, LayoutSpec, PipelineSpec, ShaderVariantKey};
30use khora_core::renderer::api::util::dynamic_uniform_buffer::DynamicUniformRingBuffer;
31use khora_core::renderer::api::util::uniform_ring_buffer::UniformRingBuffer;
32use khora_core::{
33 asset::Material,
34 renderer::{
35 api::{
36 command::{
37 LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment,
38 RenderPassDescriptor, StoreOp,
39 },
40 core::RenderContext,
41 pipeline::enums::PrimitiveTopology,
42 pipeline::RenderPipelineId,
43 scene::{
44 DirectionalLightUniform, GpuMesh, LightingUniforms, ModelUniforms,
45 PointLightUniform, SpotLightUniform, MAX_DIRECTIONAL_LIGHTS, MAX_POINT_LIGHTS,
46 MAX_SPOT_LIGHTS,
47 },
48 },
49 traits::CommandEncoder,
50 },
51};
52use khora_data::assets::Assets;
53use khora_data::render::RenderWorld;
54use std::sync::RwLock;
55
56const TRIANGLE_COST: f32 = 0.001;
58const DRAW_CALL_COST: f32 = 0.1;
59const LIGHT_COST_FACTOR: f32 = 0.05;
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
68pub enum ShaderComplexity {
69 Unlit,
72 #[default]
75 SimpleLit,
76 FullPBR,
79}
80
81impl ShaderComplexity {
82 pub fn cost_multiplier(&self) -> f32 {
87 match self {
88 ShaderComplexity::Unlit => 1.0,
89 ShaderComplexity::SimpleLit => 1.5,
90 ShaderComplexity::FullPBR => 2.5,
91 }
92 }
93
94 pub fn name(&self) -> &'static str {
96 match self {
97 ShaderComplexity::Unlit => "Unlit",
98 ShaderComplexity::SimpleLit => "SimpleLit",
99 ShaderComplexity::FullPBR => "FullPBR",
100 }
101 }
102}
103
104pub struct LitForwardLane {
123 pub shader_complexity: ShaderComplexity,
125 pub max_directional_lights: u32,
127 pub max_point_lights: u32,
129 pub max_spot_lights: u32,
131 pipeline: std::sync::OnceLock<RenderPipelineId>,
133 pipeline_system:
136 std::sync::OnceLock<std::sync::Arc<dyn khora_core::renderer::traits::PipelineSystem>>,
137 camera_layout: std::sync::OnceLock<BindGroupLayoutId>,
139 model_layout: std::sync::OnceLock<BindGroupLayoutId>,
141 material_layout: std::sync::OnceLock<BindGroupLayoutId>,
143 light_layout: std::sync::OnceLock<BindGroupLayoutId>,
145 lighting_buffer_layout: std::sync::OnceLock<BindGroupLayoutId>,
147 camera_ring: std::sync::Mutex<Option<UniformRingBuffer>>,
149 lighting_ring: std::sync::Mutex<Option<UniformRingBuffer>>,
151 model_ring: std::sync::Mutex<Option<DynamicUniformRingBuffer>>,
153}
154
155impl Default for LitForwardLane {
156 fn default() -> Self {
157 Self {
158 shader_complexity: ShaderComplexity::SimpleLit,
159 max_directional_lights: 4,
160 max_point_lights: 16,
161 max_spot_lights: 8,
162 pipeline: std::sync::OnceLock::new(),
163 pipeline_system: std::sync::OnceLock::new(),
164 camera_layout: std::sync::OnceLock::new(),
165 model_layout: std::sync::OnceLock::new(),
166 material_layout: std::sync::OnceLock::new(),
167 light_layout: std::sync::OnceLock::new(),
168 lighting_buffer_layout: std::sync::OnceLock::new(),
169 camera_ring: std::sync::Mutex::new(None),
170 lighting_ring: std::sync::Mutex::new(None),
171 model_ring: std::sync::Mutex::new(None),
172 }
173 }
174}
175
176impl LitForwardLane {
177 pub fn new() -> Self {
179 Self::default()
180 }
181
182 pub fn with_complexity(complexity: ShaderComplexity) -> Self {
184 Self {
185 shader_complexity: complexity,
186 ..Default::default()
187 }
188 }
189
190 pub fn effective_light_counts(&self, render_world: &RenderWorld) -> (usize, usize, usize) {
194 let dir_count = render_world
195 .directional_light_count()
196 .min(self.max_directional_lights as usize);
197 let point_count = render_world
198 .point_light_count()
199 .min(self.max_point_lights as usize);
200 let spot_count = render_world
201 .spot_light_count()
202 .min(self.max_spot_lights as usize);
203
204 (dir_count, point_count, spot_count)
205 }
206
207 fn light_cost_factor(&self, render_world: &RenderWorld) -> f32 {
209 let (dir_count, point_count, spot_count) = self.effective_light_counts(render_world);
210 let total_lights = dir_count + point_count + spot_count;
211
212 1.0 + (total_lights as f32 * LIGHT_COST_FACTOR)
214 }
215}
216
217impl khora_core::lane::Lane for LitForwardLane {
218 fn strategy_name(&self) -> &'static str {
219 "LitForward"
220 }
221
222 fn lane_kind(&self) -> khora_core::lane::LaneKind {
223 khora_core::lane::LaneKind::Render
224 }
225
226 fn estimate_cost(&self, ctx: &khora_core::lane::LaneContext) -> f32 {
227 let render_world = match ctx.get::<khora_core::lane::Ref<khora_data::render::RenderWorld>>()
228 {
229 Some(slot) => slot.get(),
230 None => return 1.0,
231 };
232 let gpu_meshes = match ctx.get::<std::sync::Arc<
233 std::sync::RwLock<
234 khora_data::assets::Assets<khora_core::renderer::api::scene::GpuMesh>,
235 >,
236 >>() {
237 Some(arc) => arc,
238 None => return 1.0,
239 };
240 self.estimate_render_cost(render_world, gpu_meshes)
241 }
242
243 fn on_initialize(
244 &self,
245 ctx: &mut khora_core::lane::LaneContext,
246 ) -> Result<(), khora_core::lane::LaneError> {
247 let device = ctx
248 .get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
249 .ok_or(khora_core::lane::LaneError::missing(
250 "Arc<dyn GraphicsDevice>",
251 ))?
252 .clone();
253 let pipeline_system = ctx
254 .get::<std::sync::Arc<dyn khora_core::renderer::traits::PipelineSystem>>()
255 .ok_or(khora_core::lane::LaneError::missing(
256 "Arc<dyn PipelineSystem>",
257 ))?
258 .clone();
259 let _ = self.pipeline_system.set(pipeline_system.clone());
262 self.on_gpu_init(device.as_ref(), pipeline_system.as_ref())
263 .map_err(|e| khora_core::lane::LaneError::InitializationFailed(Box::new(e)))
264 }
265
266 fn execute(
267 &self,
268 ctx: &mut khora_core::lane::LaneContext,
269 ) -> Result<(), khora_core::lane::LaneError> {
270 use khora_core::lane::{LaneError, Ref, Slot};
271 let device = ctx
272 .get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
273 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
274 .clone();
275 let gpu_meshes = ctx
276 .get::<std::sync::Arc<
277 std::sync::RwLock<
278 khora_data::assets::Assets<khora_core::renderer::api::scene::GpuMesh>,
279 >,
280 >>()
281 .ok_or(LaneError::missing("Arc<RwLock<Assets<GpuMesh>>>"))?
282 .clone();
283 let encoder = ctx
284 .get::<Slot<dyn khora_core::renderer::traits::CommandEncoder>>()
285 .ok_or(LaneError::missing("Slot<dyn CommandEncoder>"))?
286 .get();
287 let render_world = ctx
288 .get::<Ref<khora_data::render::RenderWorld>>()
289 .ok_or(LaneError::missing("Ref<RenderWorld>"))?
290 .get();
291 let color_target = ctx
292 .get::<khora_core::lane::ColorTarget>()
293 .ok_or(LaneError::missing("ColorTarget"))?
294 .0;
295 let depth_target = ctx
296 .get::<khora_core::lane::DepthTarget>()
297 .ok_or(LaneError::missing("DepthTarget"))?
298 .0;
299 let clear_color = ctx
300 .get::<khora_core::lane::ClearColor>()
301 .ok_or(LaneError::missing("ClearColor"))?
302 .0;
303 let render_ctx = khora_core::renderer::api::core::RenderContext::new(
304 &color_target,
305 Some(&depth_target),
306 clear_color,
307 );
308
309 let (shadow_entries, shadow_bindings) = ctx
315 .get::<Slot<khora_core::lane::OutputDeck>>()
316 .map(|s| {
317 let frame = s
318 .get()
319 .slot::<khora_core::renderer::api::shadow::ShadowFrame>();
320 (frame.entries.clone(), frame.bindings)
321 })
322 .unwrap_or_default();
323
324 let ibl_bindings = ctx
325 .get::<khora_core::renderer::api::ibl::IblGpuBindings>()
326 .copied();
327
328 self.render(
329 render_world,
330 &shadow_entries,
331 shadow_bindings,
332 ibl_bindings,
333 device.as_ref(),
334 encoder,
335 &render_ctx,
336 &gpu_meshes,
337 );
338 Ok(())
339 }
340
341 fn on_shutdown(&self, ctx: &mut khora_core::lane::LaneContext) {
342 if let Some(device) = ctx.get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
343 {
344 self.on_gpu_shutdown(device.as_ref());
345 }
346 }
347
348 fn as_any(&self) -> &dyn std::any::Any {
349 self
350 }
351
352 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
353 self
354 }
355}
356
357impl LitForwardLane {
358 pub fn get_pipeline_for_material(
360 &self,
361 _material: Option<&khora_core::asset::AssetHandle<Box<dyn Material>>>,
362 ) -> RenderPipelineId {
363 self.pipeline.get().copied().unwrap_or(RenderPipelineId(0))
365 }
366
367 #[allow(clippy::too_many_arguments)]
368 fn render(
369 &self,
370 render_world: &RenderWorld,
371 shadow_entries: &khora_data::render::ShadowEntries,
372 shadow_bindings: Option<khora_data::render::ShadowGpuBindings>,
373 ibl_bindings: Option<khora_core::renderer::api::ibl::IblGpuBindings>,
374 device: &dyn khora_core::renderer::GraphicsDevice,
375 encoder: &mut dyn CommandEncoder,
376 render_ctx: &RenderContext,
377 gpu_meshes: &RwLock<Assets<GpuMesh>>,
378 ) {
379 use khora_core::renderer::api::command::{
380 BindGroupDescriptor, BindGroupEntry, BindingResource, BufferBinding,
381 };
382
383 let Some(view) = render_world.views.first() else {
388 let color_attachment = khora_core::renderer::api::command::RenderPassColorAttachment {
389 view: render_ctx.color_target,
390 resolve_target: None,
391 ops: khora_core::renderer::api::command::Operations {
392 load: khora_core::renderer::api::command::LoadOp::Clear(render_ctx.clear_color),
393 store: khora_core::renderer::api::command::StoreOp::Store,
394 },
395 base_array_layer: 0,
396 base_mip_level: 0,
397 };
398 let clear_desc = khora_core::renderer::api::command::RenderPassDescriptor {
399 label: Some("LitForward Clear-Only Pass"),
400 color_attachments: &[color_attachment],
401 depth_stencil_attachment: render_ctx.depth_target.map(|depth_view| {
402 khora_core::renderer::api::command::RenderPassDepthStencilAttachment {
403 view: depth_view,
404 depth_ops: Some(khora_core::renderer::api::command::Operations {
405 load: khora_core::renderer::api::command::LoadOp::Clear(1.0),
406 store: khora_core::renderer::api::command::StoreOp::Store,
407 }),
408 stencil_ops: None,
409 base_array_layer: 0,
410 }
411 }),
412 };
413 let _ = encoder.begin_render_pass(&clear_desc);
414 return;
415 };
416
417 let camera_uniforms = khora_core::renderer::api::resource::CameraUniformData {
423 view_projection: view.view_proj.to_cols_array_2d(),
424 camera_position: [view.position.x, view.position.y, view.position.z, 1.0],
425 };
426
427 let camera_bind_group = {
428 let mut lock = crate::lock_or_log!(
429 self.camera_ring.lock(),
430 "LitForwardLane::render camera_ring"
431 );
432 let ring = match lock.as_mut() {
433 Some(r) => r,
434 None => {
435 log::warn!("LitForwardLane: camera ring buffer not initialized");
436 return;
437 }
438 };
439 ring.advance();
440 if let Err(e) = ring.write(device, bytemuck::bytes_of(&camera_uniforms)) {
441 log::error!("Failed to write camera ring buffer: {:?}", e);
442 return;
443 }
444 *ring.current_bind_group() };
446
447 let mut lighting_uniforms = LightingUniforms {
449 directional_lights: [DirectionalLightUniform {
450 direction: [0.0; 4],
451 color: khora_core::math::LinearRgba::BLACK,
452 shadow_view_proj: [[0.0; 4]; 4],
453 shadow_params: [0.0; 4],
454 }; MAX_DIRECTIONAL_LIGHTS],
455 point_lights: [PointLightUniform {
456 position: [0.0; 4],
457 color: khora_core::math::LinearRgba::BLACK,
458 shadow_params: [0.0; 4],
459 }; MAX_POINT_LIGHTS],
460 spot_lights: [SpotLightUniform {
461 position: [0.0; 4],
462 direction: [0.0; 4],
463 color: khora_core::math::LinearRgba::BLACK,
464 params: [0.0; 4],
465 shadow_view_proj: [[0.0; 4]; 4],
466 shadow_params: [0.0; 4],
467 }; MAX_SPOT_LIGHTS],
468 num_directional_lights: 0,
469 num_point_lights: 0,
470 num_spot_lights: 0,
471 _padding: 0,
472 };
473
474 for (light_index, light) in render_world.lights.iter().enumerate() {
475 let shadow = shadow_entries.get(light_index);
476
477 let (atlas2d_view_proj, atlas2d_index) = match shadow {
479 Some(khora_data::render::ShadowEntry::Atlas2D {
480 view_proj,
481 atlas_index,
482 }) => (*view_proj, *atlas_index as f32),
483 _ => (khora_core::math::Mat4::IDENTITY, -1.0),
484 };
485
486 let (cube_layer, cube_far_plane) = match shadow {
488 Some(khora_data::render::ShadowEntry::Cube {
489 cube_array_index,
490 far_plane,
491 ..
492 }) => (*cube_array_index as f32, *far_plane),
493 _ => (-1.0, 0.0),
494 };
495
496 match light.light_type {
497 khora_core::renderer::light::LightType::Directional(ref d) => {
498 if (lighting_uniforms.num_directional_lights as usize) < MAX_DIRECTIONAL_LIGHTS
499 {
500 let idx = lighting_uniforms.num_directional_lights as usize;
501 lighting_uniforms.directional_lights[idx] = DirectionalLightUniform {
502 direction: [
503 light.direction.x,
504 light.direction.y,
505 light.direction.z,
506 0.0,
507 ],
508 color: d.color.with_alpha(d.intensity),
509 shadow_view_proj: atlas2d_view_proj.to_cols_array_2d(),
510 shadow_params: [
511 atlas2d_index,
512 d.shadow_bias,
513 d.shadow_normal_bias,
514 0.0,
515 ],
516 };
517 lighting_uniforms.num_directional_lights += 1;
518 }
519 }
520 khora_core::renderer::light::LightType::Point(ref p) => {
521 if (lighting_uniforms.num_point_lights as usize) < MAX_POINT_LIGHTS {
522 let idx = lighting_uniforms.num_point_lights as usize;
523 lighting_uniforms.point_lights[idx] = PointLightUniform {
531 position: [
532 light.position.x,
533 light.position.y,
534 light.position.z,
535 p.range,
536 ],
537 color: p.color.with_alpha(p.intensity),
538 shadow_params: [
539 cube_layer,
540 p.shadow_bias,
541 p.shadow_normal_bias,
542 cube_far_plane,
543 ],
544 };
545 lighting_uniforms.num_point_lights += 1;
546 }
547 }
548 khora_core::renderer::light::LightType::Spot(ref s) => {
549 if (lighting_uniforms.num_spot_lights as usize) < MAX_SPOT_LIGHTS {
550 let idx = lighting_uniforms.num_spot_lights as usize;
551 lighting_uniforms.spot_lights[idx] = SpotLightUniform {
552 position: [
553 light.position.x,
554 light.position.y,
555 light.position.z,
556 s.range,
557 ],
558 direction: [
559 light.direction.x,
560 light.direction.y,
561 light.direction.z,
562 s.inner_cone_angle.cos(),
563 ],
564 color: s.color.with_alpha(s.intensity),
565 params: [s.outer_cone_angle.cos(), 0.0, 0.0, 0.0],
566 shadow_view_proj: atlas2d_view_proj.to_cols_array_2d(),
567 shadow_params: [
568 atlas2d_index,
569 s.shadow_bias,
570 s.shadow_normal_bias,
571 0.0,
572 ],
573 };
574 lighting_uniforms.num_spot_lights += 1;
575 }
576 }
577 }
578 }
579
580 let (_lighting_bind_group, lighting_ring_buffer_id) = {
581 let mut lock = crate::lock_or_log!(
582 self.lighting_ring.lock(),
583 "LitForwardLane::render lighting_ring"
584 );
585 let ring = match lock.as_mut() {
586 Some(r) => r,
587 None => {
588 log::warn!("LitForwardLane: lighting ring buffer not initialized");
589 return;
590 }
591 };
592 ring.advance();
593 if let Err(e) = ring.write(device, bytemuck::bytes_of(&lighting_uniforms)) {
594 log::error!("Failed to write lighting ring buffer: {:?}", e);
595 return;
596 }
597 (*ring.current_bind_group(), ring.current_buffer())
598 };
599
600 let gpu_mesh_assets =
607 crate::lock_or_log!(gpu_meshes.read(), "LitForwardLane::render gpu_meshes");
608
609 let fallback_pipeline = self.pipeline.get().copied().unwrap_or(RenderPipelineId(0));
612
613 let mut draw_commands = Vec::with_capacity(render_world.meshes.len());
617 let mut transparent_draws: Vec<(f32, khora_core::renderer::api::command::DrawCommand)> =
618 Vec::new();
619
620 let mut temp_bind_groups = Vec::new();
621
622 let mut model_ring_lock =
625 crate::lock_or_log!(self.model_ring.lock(), "LitForwardLane::render model_ring");
626 let model_ring = match model_ring_lock.as_mut() {
627 Some(r) => r,
628 None => {
629 log::warn!("LitForwardLane: model ring buffer not initialized");
630 return;
631 }
632 };
633 model_ring.advance();
634
635 for extracted_mesh in &render_world.meshes {
636 let Some(gpu_mesh_handle) = gpu_mesh_assets.get(&extracted_mesh.cpu_mesh_uuid) else {
637 continue;
638 };
639 let Some(gpu_material) = &extracted_mesh.gpu_material else {
642 continue;
643 };
644 let pipeline_id = self
649 .pipeline_system
650 .get()
651 .map(|ps| {
652 ps.pipeline(
653 device,
654 &pipeline_spec(
655 device,
656 gpu_material.variant.clone(),
657 gpu_material.double_sided,
658 gpu_material.blend,
659 ),
660 )
661 })
662 .transpose()
663 .unwrap_or_else(|e| {
664 log::error!("LitForwardLane: pipeline resolve failed: {:?}", e);
665 None
666 })
667 .unwrap_or(fallback_pipeline);
668 let model_mat = extracted_mesh.transform.to_matrix();
669 let normal_mat = if let Some(inverse) = model_mat.inverse() {
670 inverse.transpose()
671 } else {
672 continue;
673 };
674 let model_uniforms = ModelUniforms {
675 model_matrix: model_mat.to_cols_array_2d(),
676 normal_matrix: normal_mat.to_cols_array_2d(),
677 };
678 let model_offset = match model_ring.push(device, bytemuck::bytes_of(&model_uniforms)) {
679 Ok(offset) => offset,
680 Err(e) => {
681 log::error!("LitForwardLane: failed to push model uniforms: {:?}", e);
682 continue;
683 }
684 };
685 let model_bg = *model_ring.current_bind_group();
686
687 let command = khora_core::renderer::api::command::DrawCommand {
688 pipeline: pipeline_id,
689 vertex_buffer: gpu_mesh_handle.vertex_buffer,
690 index_buffer: gpu_mesh_handle.index_buffer,
691 index_format: gpu_mesh_handle.index_format,
692 index_count: gpu_mesh_handle.index_count,
693 model_bind_group: Some(model_bg),
694 model_offset,
695 material_bind_group: Some(gpu_material.bind_group),
696 material_offset: 0,
697 };
698 if gpu_material.blend {
699 transparent_draws.push((
700 crate::render_lane::camera_distance_sq(&model_mat, view.position),
701 command,
702 ));
703 } else {
704 draw_commands.push(command);
705 }
706 }
707
708 draw_commands.sort_by_key(|cmd| cmd.pipeline.0);
712 transparent_draws
715 .sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
716
717 let color_attachment = RenderPassColorAttachment {
719 view: render_ctx.color_target,
720 resolve_target: None,
721 ops: Operations {
722 load: LoadOp::Clear(render_ctx.clear_color),
723 store: StoreOp::Store,
724 },
725 base_array_layer: 0,
726 base_mip_level: 0,
727 };
728
729 let render_pass_desc = RenderPassDescriptor {
730 label: Some("Lit Forward Pass"),
731 color_attachments: &[color_attachment],
732 depth_stencil_attachment: render_ctx.depth_target.map(|depth_view| {
733 RenderPassDepthStencilAttachment {
734 view: depth_view,
735 depth_ops: Some(Operations {
736 load: LoadOp::Clear(1.0),
737 store: StoreOp::Store,
738 }),
739 stencil_ops: None,
740 base_array_layer: 0,
741 }
742 }),
743 };
744
745 let final_lighting_bind_group = if let Some(layout) = self.light_layout.get().copied() {
750 let Some(shadow_bindings) = shadow_bindings else {
751 log::warn!(
752 "LitForwardLane: ShadowGpuBindings not available (shadow agent inactive?), skipping lit render"
753 );
754 return;
755 };
756
757 let mut entries = vec![BindGroupEntry {
759 binding: khora_data::render::shadow_bindings::binding::LIGHTING_UNIFORMS,
760 resource: BindingResource::Buffer(BufferBinding {
761 buffer: lighting_ring_buffer_id,
762 offset: 0,
763 size: None,
764 }),
765 _phantom: std::marker::PhantomData,
766 }];
767 khora_data::render::shadow_bindings::fill_shadow_bind_group_entries(
769 &shadow_bindings,
770 &mut entries,
771 );
772 let Some(ibl) = ibl_bindings else {
774 log::warn!("LitForwardLane: IBL bindings not available, skipping lit render");
775 return;
776 };
777 khora_core::renderer::api::ibl::fill_ibl_bind_group_entries(&ibl, 4, &mut entries);
778
779 match device.create_bind_group(&BindGroupDescriptor {
780 label: Some("lit_forward_lighting_bind_group_dynamic"),
781 layout,
782 entries: &entries,
783 }) {
784 Ok(bg) => {
785 temp_bind_groups.push(bg);
786 bg
787 }
788 Err(e) => {
789 log::error!(
790 "LitForwardLane: Failed to create lighting bind group: {:?}",
791 e
792 );
793 return;
794 }
795 }
796 } else {
797 log::warn!("LitForwardLane: light layout not initialized");
798 return;
799 };
800
801 let mut render_pass = encoder.begin_render_pass(&render_pass_desc);
802
803 render_pass.set_bind_group(0, &camera_bind_group, &[]);
805 render_pass.set_bind_group(3, &final_lighting_bind_group, &[]);
806
807 let mut current_pipeline: Option<RenderPipelineId> = None;
810
811 for cmd in draw_commands
812 .iter()
813 .chain(transparent_draws.iter().map(|(_, cmd)| cmd))
814 {
815 if current_pipeline != Some(cmd.pipeline) {
816 render_pass.set_pipeline(&cmd.pipeline);
817 current_pipeline = Some(cmd.pipeline);
818 }
819
820 if let Some(bg) = &cmd.model_bind_group {
821 render_pass.set_bind_group(1, bg, &[cmd.model_offset]);
822 }
823
824 if let Some(bg) = &cmd.material_bind_group {
825 render_pass.set_bind_group(2, bg, &[]);
826 }
827
828 render_pass.set_vertex_buffer(0, &cmd.vertex_buffer, 0);
829 render_pass.set_index_buffer(&cmd.index_buffer, 0, cmd.index_format);
830
831 render_pass.draw_indexed(0..cmd.index_count, 0, 0..1);
832 }
833
834 for bg in temp_bind_groups {
837 let _ = device.destroy_bind_group(bg);
838 }
839 }
840
841 fn estimate_render_cost(
842 &self,
843 render_world: &RenderWorld,
844 gpu_meshes: &RwLock<Assets<GpuMesh>>,
845 ) -> f32 {
846 let gpu_mesh_assets = crate::lock_or_log!(
847 gpu_meshes.read(),
848 "LitForwardLane::estimate_render_cost",
849 0.0
850 );
851
852 let mut total_triangles = 0u32;
853 let mut draw_call_count = 0u32;
854
855 for extracted_mesh in &render_world.meshes {
856 if let Some(gpu_mesh) = gpu_mesh_assets.get(&extracted_mesh.cpu_mesh_uuid) {
857 let triangle_count = match gpu_mesh.primitive_topology {
859 PrimitiveTopology::TriangleList => gpu_mesh.index_count / 3,
860 PrimitiveTopology::TriangleStrip => {
861 if gpu_mesh.index_count >= 3 {
862 gpu_mesh.index_count - 2
863 } else {
864 0
865 }
866 }
867 PrimitiveTopology::LineList
868 | PrimitiveTopology::LineStrip
869 | PrimitiveTopology::PointList => 0,
870 };
871
872 total_triangles += triangle_count;
873 draw_call_count += 1;
874 }
875 }
876
877 let base_cost =
879 (total_triangles as f32 * TRIANGLE_COST) + (draw_call_count as f32 * DRAW_CALL_COST);
880
881 let shader_factor = self.shader_complexity.cost_multiplier();
883
884 let light_factor = self.light_cost_factor(render_world);
886
887 base_cost * shader_factor * light_factor
889 }
890
891 fn on_gpu_init(
892 &self,
893 device: &dyn khora_core::renderer::GraphicsDevice,
894 pipeline_system: &dyn khora_core::renderer::traits::PipelineSystem,
895 ) -> Result<(), khora_core::renderer::error::RenderError> {
896 use crate::render_lane::util::lock::mutex_lock_render;
897
898 log::info!("LitForwardLane: Initializing GPU resources...");
899
900 let variant = ShaderVariantKey::empty();
903 let camera_layout = pipeline_system.layout(device, LayoutKey::Camera, &variant)?;
904 let model_layout = pipeline_system.layout(device, LayoutKey::Model, &variant)?;
905 let material_layout = pipeline_system.layout(device, LayoutKey::Material, &variant)?;
906 let light_layout = pipeline_system.layout(device, LayoutKey::Lighting, &variant)?;
907 let lighting_buffer_layout =
908 pipeline_system.layout(device, LayoutKey::LightingBuffer, &variant)?;
909
910 let pipeline_id = pipeline_system.pipeline(
914 device,
915 &pipeline_spec(device, ShaderVariantKey::empty(), false, false),
916 )?;
917
918 let _ = self.camera_layout.set(camera_layout);
921 let _ = self.model_layout.set(model_layout);
922 let _ = self.material_layout.set(material_layout);
923 let _ = self.light_layout.set(light_layout);
924 let _ = self.lighting_buffer_layout.set(lighting_buffer_layout);
925 let _ = self.pipeline.set(pipeline_id);
926
927 let camera_ring = UniformRingBuffer::new(
931 device,
932 camera_layout,
933 0,
934 std::mem::size_of::<khora_core::renderer::api::resource::CameraUniformData>() as u64,
935 "Camera Uniform Ring",
936 )
937 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
938
939 let lighting_ring = UniformRingBuffer::new(
940 device,
941 lighting_buffer_layout,
942 0,
943 std::mem::size_of::<LightingUniforms>() as u64,
944 "Lighting Uniform Ring",
945 )
946 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
947
948 let model_ring = DynamicUniformRingBuffer::new(
949 device,
950 model_layout,
951 0,
952 std::mem::size_of::<ModelUniforms>() as u32,
953 khora_core::renderer::api::util::dynamic_uniform_buffer::DEFAULT_MAX_ELEMENTS,
954 khora_core::renderer::api::util::dynamic_uniform_buffer::MIN_UNIFORM_ALIGNMENT,
955 "LitForward Model Ring",
956 )
957 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
958
959 *mutex_lock_render(&self.camera_ring, "LitForward init.camera_ring")? = Some(camera_ring);
960 *mutex_lock_render(&self.lighting_ring, "LitForward init.lighting_ring")? =
961 Some(lighting_ring);
962 *mutex_lock_render(&self.model_ring, "LitForward init.model_ring")? = Some(model_ring);
963
964 log::info!(
965 "LitForwardLane: Persistent ring buffers created (camera: {} bytes, lighting: {} bytes, {} slots each)",
966 std::mem::size_of::<khora_core::renderer::api::resource::CameraUniformData>(),
967 std::mem::size_of::<LightingUniforms>(),
968 khora_core::renderer::api::core::MAX_FRAMES_IN_FLIGHT,
969 );
970
971 Ok(())
972 }
973
974 fn on_gpu_shutdown(&self, device: &dyn khora_core::renderer::GraphicsDevice) {
975 if let Some(ring) = self.camera_ring.lock().ok().and_then(|mut g| g.take()) {
983 ring.destroy(device);
984 }
985 if let Some(ring) = self.lighting_ring.lock().ok().and_then(|mut g| g.take()) {
986 ring.destroy(device);
987 }
988 }
989}
990
991fn pipeline_spec(
999 device: &dyn khora_core::renderer::GraphicsDevice,
1000 variant: ShaderVariantKey,
1001 double_sided: bool,
1002 blend: bool,
1003) -> PipelineSpec {
1004 use khora_core::renderer::api::pipeline::enums::{
1005 CompareFunction, CullMode, VertexFormat, VertexStepMode,
1006 };
1007 use khora_core::renderer::api::pipeline::state::{
1008 BlendStateDescriptor, ColorWrites, DepthBiasState, StencilFaceState,
1009 };
1010 use khora_core::renderer::api::pipeline::{
1011 ColorTargetStateDescriptor, DepthStencilStateDescriptor, MultisampleStateDescriptor,
1012 PrimitiveStateDescriptor, VertexAttributeDescriptor, VertexBufferLayoutDescriptor,
1013 };
1014 use khora_core::renderer::api::util::{SampleCount, TextureFormat};
1015 use std::borrow::Cow;
1016
1017 PipelineSpec {
1018 label: "LitForward Pipeline",
1019 shader: "khora::pipelines::lit_forward",
1020 variant,
1021 bind_group_layouts: vec![
1022 LayoutSpec::Named(LayoutKey::Camera),
1023 LayoutSpec::Named(LayoutKey::Model),
1024 LayoutSpec::Named(LayoutKey::Material),
1025 LayoutSpec::Named(LayoutKey::Lighting),
1026 ],
1027 vertex_buffers: vec![VertexBufferLayoutDescriptor {
1028 array_stride: 32,
1029 step_mode: VertexStepMode::Vertex,
1030 attributes: Cow::Owned(vec![
1031 VertexAttributeDescriptor {
1032 format: VertexFormat::Float32x3,
1033 offset: 0,
1034 shader_location: 0,
1035 },
1036 VertexAttributeDescriptor {
1037 format: VertexFormat::Float32x3,
1038 offset: 12,
1039 shader_location: 1,
1040 },
1041 VertexAttributeDescriptor {
1042 format: VertexFormat::Float32x2,
1043 offset: 24,
1044 shader_location: 2,
1045 },
1046 ]),
1047 }],
1048 vs_entry: "vs_main",
1049 fs_entry: Some("fs_main"),
1050 primitive: PrimitiveStateDescriptor {
1051 topology: PrimitiveTopology::TriangleList,
1052 cull_mode: if double_sided {
1055 None
1056 } else {
1057 Some(CullMode::Back)
1058 },
1059 ..Default::default()
1060 },
1061 depth_stencil: Some(DepthStencilStateDescriptor {
1062 format: TextureFormat::Depth32Float,
1063 depth_write_enabled: !blend,
1066 depth_compare: CompareFunction::Less,
1067 stencil_front: StencilFaceState::default(),
1068 stencil_back: StencilFaceState::default(),
1069 stencil_read_mask: 0,
1070 stencil_write_mask: 0,
1071 bias: DepthBiasState::default(),
1072 }),
1073 color_targets: vec![ColorTargetStateDescriptor {
1074 format: device
1075 .get_surface_format()
1076 .unwrap_or(TextureFormat::Rgba8UnormSrgb),
1077 blend: blend.then(BlendStateDescriptor::alpha_blending),
1078 write_mask: ColorWrites::ALL,
1079 }],
1080 multisample: MultisampleStateDescriptor {
1081 count: SampleCount::X1,
1082 mask: !0,
1083 alpha_to_coverage_enabled: false,
1084 },
1085 }
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090 use super::*;
1091 use khora_core::lane::Lane;
1092 use khora_core::{
1093 asset::{AssetHandle, AssetUUID},
1094 math::{affine_transform::AffineTransform, Mat4},
1095 renderer::{
1096 api::{pipeline::enums::PrimitiveTopology, resource::BufferId, util::IndexFormat},
1097 light::DirectionalLight,
1098 },
1099 };
1100 use khora_data::render::{ExtractedLight, ExtractedMesh};
1101 use std::sync::Arc;
1102
1103 fn create_test_gpu_mesh(index_count: u32) -> GpuMesh {
1104 GpuMesh {
1105 vertex_buffer: BufferId(0),
1106 index_buffer: BufferId(1),
1107 index_count,
1108 index_format: IndexFormat::Uint32,
1109 primitive_topology: PrimitiveTopology::TriangleList,
1110 }
1111 }
1112
1113 #[test]
1114 fn test_lit_forward_lane_creation() {
1115 let lane = LitForwardLane::new();
1116 assert_eq!(lane.strategy_name(), "LitForward");
1117 assert_eq!(lane.shader_complexity, ShaderComplexity::SimpleLit);
1118 }
1119
1120 #[test]
1121 fn test_lit_forward_lane_with_complexity() {
1122 let lane = LitForwardLane::with_complexity(ShaderComplexity::FullPBR);
1123 assert_eq!(lane.shader_complexity, ShaderComplexity::FullPBR);
1124 }
1125
1126 #[test]
1127 fn test_shader_complexity_ordering() {
1128 assert!(ShaderComplexity::Unlit < ShaderComplexity::SimpleLit);
1129 assert!(ShaderComplexity::SimpleLit < ShaderComplexity::FullPBR);
1130 }
1131
1132 #[test]
1133 fn test_shader_complexity_cost_multipliers() {
1134 assert_eq!(ShaderComplexity::Unlit.cost_multiplier(), 1.0);
1135 assert_eq!(ShaderComplexity::SimpleLit.cost_multiplier(), 1.5);
1136 assert_eq!(ShaderComplexity::FullPBR.cost_multiplier(), 2.5);
1137 }
1138
1139 #[test]
1140 fn test_cost_estimation_empty_world() {
1141 let lane = LitForwardLane::new();
1142 let render_world = RenderWorld::default();
1143 let gpu_meshes = Arc::new(RwLock::new(Assets::<GpuMesh>::new()));
1144
1145 let cost = lane.estimate_render_cost(&render_world, &gpu_meshes);
1146 assert_eq!(cost, 0.0, "Empty world should have zero cost");
1147 }
1148
1149 #[test]
1150 fn test_cost_estimation_with_meshes() {
1151 let lane = LitForwardLane::new();
1152
1153 let mesh_uuid = AssetUUID::new();
1155 let gpu_mesh = create_test_gpu_mesh(300);
1156
1157 let mut gpu_meshes = Assets::<GpuMesh>::new();
1158 gpu_meshes.insert(mesh_uuid, AssetHandle::new(gpu_mesh));
1159
1160 let mut render_world = RenderWorld::default();
1161 render_world.meshes.push(ExtractedMesh {
1162 transform: AffineTransform::default(),
1163 cpu_mesh_uuid: mesh_uuid,
1164 gpu_mesh: AssetHandle::new(create_test_gpu_mesh(300)),
1165 material: None,
1166 gpu_material: None,
1167 });
1168
1169 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
1170 let cost = lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
1171
1172 assert!(
1176 (cost - 0.3).abs() < 0.0001,
1177 "Cost should be 0.3 for 100 triangles with SimpleLit complexity, got {}",
1178 cost
1179 );
1180 }
1181
1182 #[test]
1183 fn test_cost_estimation_with_lights() {
1184 use khora_core::{
1185 math::{Mat4, Vec3},
1186 renderer::light::LightType,
1187 };
1188
1189 let lane = LitForwardLane::new();
1190
1191 let mesh_uuid = AssetUUID::new();
1193 let gpu_mesh = create_test_gpu_mesh(300);
1194
1195 let mut gpu_meshes = Assets::<GpuMesh>::new();
1196 gpu_meshes.insert(mesh_uuid, AssetHandle::new(gpu_mesh));
1197
1198 let mut render_world = RenderWorld::default();
1199 render_world.meshes.push(ExtractedMesh {
1200 transform: AffineTransform::default(),
1201 cpu_mesh_uuid: mesh_uuid,
1202 gpu_mesh: AssetHandle::new(create_test_gpu_mesh(300)),
1203 material: None,
1204 gpu_material: None,
1205 });
1206
1207 for _ in 0..4 {
1209 render_world.lights.push(ExtractedLight {
1210 light_type: LightType::Directional(DirectionalLight::default()),
1211 position: Vec3::ZERO,
1212 direction: Vec3::new(0.0, -1.0, 0.0),
1213 shadow_view_proj: Mat4::IDENTITY,
1214 shadow_atlas_index: None,
1215 });
1216 }
1217
1218 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
1219 let cost = lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
1220
1221 assert!(
1226 (cost - 0.36).abs() < 0.0001,
1227 "Cost should be 0.36 with 4 lights, got {}",
1228 cost
1229 );
1230 }
1231
1232 #[test]
1233 fn test_cost_increases_with_complexity() {
1234 let mesh_uuid = AssetUUID::new();
1235 let gpu_mesh = create_test_gpu_mesh(300);
1236
1237 let mut gpu_meshes = Assets::<GpuMesh>::new();
1238 gpu_meshes.insert(mesh_uuid, AssetHandle::new(gpu_mesh));
1239
1240 let mut render_world = RenderWorld::default();
1241 render_world.meshes.push(ExtractedMesh {
1242 transform: AffineTransform::default(),
1243 cpu_mesh_uuid: mesh_uuid,
1244 gpu_mesh: AssetHandle::new(create_test_gpu_mesh(300)),
1245 material: None,
1246 gpu_material: None,
1247 });
1248
1249 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
1250
1251 let unlit_lane = LitForwardLane::with_complexity(ShaderComplexity::Unlit);
1252 let simple_lane = LitForwardLane::with_complexity(ShaderComplexity::SimpleLit);
1253 let pbr_lane = LitForwardLane::with_complexity(ShaderComplexity::FullPBR);
1254
1255 let unlit_cost = unlit_lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
1256 let simple_cost = simple_lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
1257 let pbr_cost = pbr_lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
1258
1259 assert!(
1260 unlit_cost < simple_cost,
1261 "Unlit should be cheaper than SimpleLit"
1262 );
1263 assert!(
1264 simple_cost < pbr_cost,
1265 "SimpleLit should be cheaper than PBR"
1266 );
1267 }
1268
1269 #[test]
1270 fn test_effective_light_counts() {
1271 use khora_core::{
1272 math::Vec3,
1273 renderer::light::{LightType, PointLight},
1274 };
1275
1276 let lane = LitForwardLane {
1277 max_directional_lights: 2,
1278 max_point_lights: 4,
1279 max_spot_lights: 2,
1280 ..Default::default()
1281 };
1282
1283 let mut render_world = RenderWorld::default();
1284
1285 for _ in 0..5 {
1287 render_world.lights.push(ExtractedLight {
1288 light_type: LightType::Directional(DirectionalLight::default()),
1289 position: Vec3::ZERO,
1290 direction: Vec3::new(0.0, -1.0, 0.0),
1291 shadow_view_proj: Mat4::IDENTITY,
1292 shadow_atlas_index: None,
1293 });
1294 }
1295
1296 for _ in 0..3 {
1298 render_world.lights.push(ExtractedLight {
1299 light_type: LightType::Point(PointLight::default()),
1300 position: Vec3::ZERO,
1301 direction: Vec3::ZERO,
1302 shadow_view_proj: Mat4::IDENTITY,
1303 shadow_atlas_index: None,
1304 });
1305 }
1306
1307 let (dir, point, spot) = lane.effective_light_counts(&render_world);
1308 assert_eq!(dir, 2, "Should be clamped to max 2 directional lights");
1309 assert_eq!(point, 3, "Should use all 3 point lights (under max)");
1310 assert_eq!(spot, 0, "Should have 0 spot lights");
1311 }
1312
1313 #[test]
1314 fn test_get_pipeline_for_material() {
1315 let lane = LitForwardLane::new();
1316
1317 let pipeline = lane.get_pipeline_for_material(None);
1319 assert_eq!(pipeline, RenderPipelineId(0));
1320
1321 let pipeline = lane.get_pipeline_for_material(None);
1323 assert_eq!(pipeline, RenderPipelineId(0));
1324 }
1325}