1#[allow(unused_imports)]
28use khora_core::math::{Extent2D, Extent3D, LinearRgba, Mat4, Origin3D};
29#[allow(unused_imports)]
30use khora_core::renderer::api::command::BindGroupLayoutId;
31
32use super::RenderWorld;
33use khora_core::renderer::api::util::uniform_ring_buffer::UniformRingBuffer;
34use khora_core::{
35 asset::Material,
36 renderer::{
37 api::{
38 command::{
39 LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment,
40 RenderPassDescriptor, StoreOp,
41 },
42 core::RenderContext,
43 pipeline::enums::PrimitiveTopology,
44 pipeline::RenderPipelineId,
45 scene::{
46 DirectionalLightUniform, GpuMesh, LightingUniforms, MaterialUniforms,
47 ModelUniforms, PointLightUniform, SpotLightUniform, MAX_DIRECTIONAL_LIGHTS,
48 MAX_POINT_LIGHTS, MAX_SPOT_LIGHTS,
49 },
50 },
51 traits::CommandEncoder,
52 },
53};
54use khora_data::assets::Assets;
55use std::sync::RwLock;
56
57const TRIANGLE_COST: f32 = 0.001;
59const DRAW_CALL_COST: f32 = 0.1;
60const LIGHT_COST_FACTOR: f32 = 0.05;
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
69pub enum ShaderComplexity {
70 Unlit,
73 #[default]
76 SimpleLit,
77 FullPBR,
80}
81
82impl ShaderComplexity {
83 pub fn cost_multiplier(&self) -> f32 {
88 match self {
89 ShaderComplexity::Unlit => 1.0,
90 ShaderComplexity::SimpleLit => 1.5,
91 ShaderComplexity::FullPBR => 2.5,
92 }
93 }
94
95 pub fn name(&self) -> &'static str {
97 match self {
98 ShaderComplexity::Unlit => "Unlit",
99 ShaderComplexity::SimpleLit => "SimpleLit",
100 ShaderComplexity::FullPBR => "FullPBR",
101 }
102 }
103}
104
105#[derive(Debug)]
124pub struct LitForwardLane {
125 pub shader_complexity: ShaderComplexity,
127 pub max_directional_lights: u32,
129 pub max_point_lights: u32,
131 pub max_spot_lights: u32,
133 pipeline: std::sync::Mutex<Option<RenderPipelineId>>,
135 camera_layout: std::sync::Mutex<Option<BindGroupLayoutId>>,
137 model_layout: std::sync::Mutex<Option<BindGroupLayoutId>>,
139 material_layout: std::sync::Mutex<Option<BindGroupLayoutId>>,
141 light_layout: std::sync::Mutex<Option<BindGroupLayoutId>>,
143 lighting_buffer_layout: std::sync::Mutex<Option<BindGroupLayoutId>>,
145 camera_ring: std::sync::Mutex<Option<UniformRingBuffer>>,
147 lighting_ring: std::sync::Mutex<Option<UniformRingBuffer>>,
149}
150
151impl Default for LitForwardLane {
152 fn default() -> Self {
153 Self {
154 shader_complexity: ShaderComplexity::SimpleLit,
155 max_directional_lights: 4,
156 max_point_lights: 16,
157 max_spot_lights: 8,
158 pipeline: std::sync::Mutex::new(None),
159 camera_layout: std::sync::Mutex::new(None),
160 model_layout: std::sync::Mutex::new(None),
161 material_layout: std::sync::Mutex::new(None),
162 light_layout: std::sync::Mutex::new(None),
163 lighting_buffer_layout: std::sync::Mutex::new(None),
164 camera_ring: std::sync::Mutex::new(None),
165 lighting_ring: std::sync::Mutex::new(None),
166 }
167 }
168}
169
170impl LitForwardLane {
171 pub fn new() -> Self {
173 Self::default()
174 }
175
176 pub fn with_complexity(complexity: ShaderComplexity) -> Self {
178 Self {
179 shader_complexity: complexity,
180 ..Default::default()
181 }
182 }
183
184 pub fn effective_light_counts(&self, render_world: &RenderWorld) -> (usize, usize, usize) {
188 let dir_count = render_world
189 .directional_light_count()
190 .min(self.max_directional_lights as usize);
191 let point_count = render_world
192 .point_light_count()
193 .min(self.max_point_lights as usize);
194 let spot_count = render_world
195 .spot_light_count()
196 .min(self.max_spot_lights as usize);
197
198 (dir_count, point_count, spot_count)
199 }
200
201 fn light_cost_factor(&self, render_world: &RenderWorld) -> f32 {
203 let (dir_count, point_count, spot_count) = self.effective_light_counts(render_world);
204 let total_lights = dir_count + point_count + spot_count;
205
206 1.0 + (total_lights as f32 * LIGHT_COST_FACTOR)
208 }
209}
210
211impl khora_core::lane::Lane for LitForwardLane {
212 fn strategy_name(&self) -> &'static str {
213 "LitForward"
214 }
215
216 fn lane_kind(&self) -> khora_core::lane::LaneKind {
217 khora_core::lane::LaneKind::Render
218 }
219
220 fn estimate_cost(&self, ctx: &khora_core::lane::LaneContext) -> f32 {
221 let render_world =
222 match ctx.get::<khora_core::lane::Slot<crate::render_lane::RenderWorld>>() {
223 Some(slot) => slot.get_ref(),
224 None => return 1.0,
225 };
226 let gpu_meshes = match ctx.get::<std::sync::Arc<
227 std::sync::RwLock<
228 khora_data::assets::Assets<khora_core::renderer::api::scene::GpuMesh>,
229 >,
230 >>() {
231 Some(arc) => arc,
232 None => return 1.0,
233 };
234 self.estimate_render_cost(render_world, gpu_meshes)
235 }
236
237 fn on_initialize(
238 &self,
239 ctx: &mut khora_core::lane::LaneContext,
240 ) -> Result<(), khora_core::lane::LaneError> {
241 let device = ctx
242 .get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
243 .ok_or(khora_core::lane::LaneError::missing(
244 "Arc<dyn GraphicsDevice>",
245 ))?;
246 self.on_gpu_init(device.as_ref())
247 .map_err(|e| khora_core::lane::LaneError::InitializationFailed(Box::new(e)))
248 }
249
250 fn execute(
251 &self,
252 ctx: &mut khora_core::lane::LaneContext,
253 ) -> Result<(), khora_core::lane::LaneError> {
254 use khora_core::lane::{LaneError, Slot};
255 let device = ctx
256 .get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
257 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
258 .clone();
259 let gpu_meshes = ctx
260 .get::<std::sync::Arc<
261 std::sync::RwLock<
262 khora_data::assets::Assets<khora_core::renderer::api::scene::GpuMesh>,
263 >,
264 >>()
265 .ok_or(LaneError::missing("Arc<RwLock<Assets<GpuMesh>>>"))?
266 .clone();
267 let encoder = ctx
268 .get::<Slot<dyn khora_core::renderer::traits::CommandEncoder>>()
269 .ok_or(LaneError::missing("Slot<dyn CommandEncoder>"))?
270 .get();
271 let render_world = ctx
272 .get::<Slot<crate::render_lane::RenderWorld>>()
273 .ok_or(LaneError::missing("Slot<RenderWorld>"))?
274 .get_ref();
275 let color_target = ctx
276 .get::<khora_core::lane::ColorTarget>()
277 .ok_or(LaneError::missing("ColorTarget"))?
278 .0;
279 let depth_target = ctx
280 .get::<khora_core::lane::DepthTarget>()
281 .ok_or(LaneError::missing("DepthTarget"))?
282 .0;
283 let clear_color = ctx
284 .get::<khora_core::lane::ClearColor>()
285 .ok_or(LaneError::missing("ClearColor"))?
286 .0;
287 let shadow_atlas = ctx.get::<khora_core::lane::ShadowAtlasView>().map(|v| v.0);
288 let shadow_sampler = ctx
289 .get::<khora_core::lane::ShadowComparisonSampler>()
290 .map(|v| v.0);
291
292 let mut render_ctx = khora_core::renderer::api::core::RenderContext::new(
293 &color_target,
294 Some(&depth_target),
295 clear_color,
296 );
297 render_ctx.shadow_atlas = shadow_atlas.as_ref();
298 render_ctx.shadow_sampler = shadow_sampler.as_ref();
299
300 self.render(
301 render_world,
302 device.as_ref(),
303 encoder,
304 &render_ctx,
305 &gpu_meshes,
306 );
307 Ok(())
308 }
309
310 fn on_shutdown(&self, ctx: &mut khora_core::lane::LaneContext) {
311 if let Some(device) = ctx.get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
312 {
313 self.on_gpu_shutdown(device.as_ref());
314 }
315 }
316
317 fn as_any(&self) -> &dyn std::any::Any {
318 self
319 }
320
321 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
322 self
323 }
324}
325
326impl LitForwardLane {
327 pub fn get_pipeline_for_material(
329 &self,
330 _material: Option<&khora_core::asset::AssetHandle<Box<dyn Material>>>,
331 ) -> RenderPipelineId {
332 self.pipeline.lock().unwrap().unwrap_or(RenderPipelineId(0))
334 }
335
336 fn render(
337 &self,
338 render_world: &RenderWorld,
339 device: &dyn khora_core::renderer::GraphicsDevice,
340 encoder: &mut dyn CommandEncoder,
341 render_ctx: &RenderContext,
342 gpu_meshes: &RwLock<Assets<GpuMesh>>,
343 ) {
344 use khora_core::renderer::api::{
345 command::{BindGroupDescriptor, BindGroupEntry, BindingResource, BufferBinding},
346 resource::{BufferDescriptor, BufferUsage},
347 };
348
349 let view = if let Some(first_view) = render_world.views.first() {
351 first_view
352 } else {
353 return; };
355
356 let camera_uniforms = khora_core::renderer::api::resource::CameraUniformData {
362 view_projection: view.view_proj.to_cols_array_2d(),
363 camera_position: [view.position.x, view.position.y, view.position.z, 1.0],
364 };
365
366 let camera_bind_group = {
367 let mut lock = self.camera_ring.lock().unwrap();
368 let ring = match lock.as_mut() {
369 Some(r) => r,
370 None => {
371 log::warn!("LitForwardLane: camera ring buffer not initialized");
372 return;
373 }
374 };
375 ring.advance();
376 if let Err(e) = ring.write(device, bytemuck::bytes_of(&camera_uniforms)) {
377 log::error!("Failed to write camera ring buffer: {:?}", e);
378 return;
379 }
380 *ring.current_bind_group() };
382
383 let mut lighting_uniforms = LightingUniforms {
385 directional_lights: [DirectionalLightUniform {
386 direction: [0.0; 4],
387 color: khora_core::math::LinearRgba::BLACK,
388 shadow_view_proj: [[0.0; 4]; 4],
389 shadow_params: [0.0; 4],
390 }; MAX_DIRECTIONAL_LIGHTS],
391 point_lights: [PointLightUniform {
392 position: [0.0; 4],
393 color: khora_core::math::LinearRgba::BLACK,
394 shadow_params: [0.0; 4],
395 }; MAX_POINT_LIGHTS],
396 spot_lights: [SpotLightUniform {
397 position: [0.0; 4],
398 direction: [0.0; 4],
399 color: khora_core::math::LinearRgba::BLACK,
400 params: [0.0; 4],
401 shadow_view_proj: [[0.0; 4]; 4],
402 shadow_params: [0.0; 4],
403 }; MAX_SPOT_LIGHTS],
404 num_directional_lights: 0,
405 num_point_lights: 0,
406 num_spot_lights: 0,
407 _padding: 0,
408 };
409
410 for light in &render_world.lights {
411 match light.light_type {
412 khora_core::renderer::light::LightType::Directional(ref d) => {
413 if (lighting_uniforms.num_directional_lights as usize) < MAX_DIRECTIONAL_LIGHTS
414 {
415 let idx = lighting_uniforms.num_directional_lights as usize;
416 let shadow_index = light.shadow_atlas_index.unwrap_or(-1) as f32;
417 lighting_uniforms.directional_lights[idx] = DirectionalLightUniform {
418 direction: [
419 light.direction.x,
420 light.direction.y,
421 light.direction.z,
422 0.0,
423 ],
424 color: d.color.with_alpha(d.intensity),
425 shadow_view_proj: light.shadow_view_proj.to_cols_array_2d(),
426 shadow_params: [shadow_index, d.shadow_bias, d.shadow_normal_bias, 0.0],
427 };
428 lighting_uniforms.num_directional_lights += 1;
429 }
430 }
431 khora_core::renderer::light::LightType::Point(ref p) => {
432 if (lighting_uniforms.num_point_lights as usize) < MAX_POINT_LIGHTS {
433 let idx = lighting_uniforms.num_point_lights as usize;
434 let shadow_index = light.shadow_atlas_index.unwrap_or(-1) as f32;
435 lighting_uniforms.point_lights[idx] = PointLightUniform {
436 position: [
437 light.position.x,
438 light.position.y,
439 light.position.z,
440 p.range,
441 ],
442 color: p.color.with_alpha(p.intensity),
443 shadow_params: [shadow_index, p.shadow_bias, p.shadow_normal_bias, 0.0],
444 };
445 lighting_uniforms.num_point_lights += 1;
446 }
447 }
448 khora_core::renderer::light::LightType::Spot(ref s) => {
449 if (lighting_uniforms.num_spot_lights as usize) < MAX_SPOT_LIGHTS {
450 let idx = lighting_uniforms.num_spot_lights as usize;
451 let shadow_index = light.shadow_atlas_index.unwrap_or(-1) as f32;
452 lighting_uniforms.spot_lights[idx] = SpotLightUniform {
453 position: [
454 light.position.x,
455 light.position.y,
456 light.position.z,
457 s.range,
458 ],
459 direction: [
460 light.direction.x,
461 light.direction.y,
462 light.direction.z,
463 s.inner_cone_angle.cos(),
464 ],
465 color: s.color.with_alpha(s.intensity),
466 params: [s.outer_cone_angle.cos(), 0.0, 0.0, 0.0],
467 shadow_view_proj: light.shadow_view_proj.to_cols_array_2d(),
468 shadow_params: [shadow_index, s.shadow_bias, s.shadow_normal_bias, 0.0],
469 };
470 lighting_uniforms.num_spot_lights += 1;
471 }
472 }
473 }
474 }
475
476 let (_lighting_bind_group, lighting_ring_buffer_id) = {
477 let mut lock = self.lighting_ring.lock().unwrap();
478 let ring = match lock.as_mut() {
479 Some(r) => r,
480 None => {
481 log::warn!("LitForwardLane: lighting ring buffer not initialized");
482 return;
483 }
484 };
485 ring.advance();
486 if let Err(e) = ring.write(device, bytemuck::bytes_of(&lighting_uniforms)) {
487 log::error!("Failed to write lighting ring buffer: {:?}", e);
488 return;
489 }
490 (*ring.current_bind_group(), ring.current_buffer())
491 };
492
493 let gpu_mesh_assets = gpu_meshes.read().unwrap();
500
501 let pipeline_id = self.pipeline.lock().unwrap().unwrap_or(RenderPipelineId(0));
503
504 let mut draw_commands = Vec::with_capacity(render_world.meshes.len());
506
507 let mut temp_buffers = Vec::new();
508 let mut temp_bind_groups = Vec::new();
509
510 for extracted_mesh in &render_world.meshes {
511 if let Some(gpu_mesh_handle) = gpu_mesh_assets.get(&extracted_mesh.cpu_mesh_uuid) {
512 let model_mat = extracted_mesh.transform.to_matrix();
514
515 let normal_mat = if let Some(inverse) = model_mat.inverse() {
517 inverse.transpose()
518 } else {
519 continue;
520 };
521
522 let mut base_color = khora_core::math::LinearRgba::WHITE;
523 let mut emissive = khora_core::math::LinearRgba::BLACK;
524 let mut specular_power = 32.0;
525
526 if let Some(mat_handle) = &extracted_mesh.material {
527 base_color = mat_handle.base_color();
528 emissive = mat_handle.emissive_color();
529 specular_power = mat_handle.specular_power();
530 }
531
532 let model_uniforms = ModelUniforms {
533 model_matrix: model_mat.to_cols_array_2d(),
534 normal_matrix: normal_mat.to_cols_array_2d(),
535 };
536
537 let model_buffer = match device.create_buffer_with_data(
538 &BufferDescriptor {
539 label: None,
540 size: std::mem::size_of::<ModelUniforms>() as u64,
541 usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
542 mapped_at_creation: false,
543 },
544 bytemuck::bytes_of(&model_uniforms),
545 ) {
546 Ok(b) => {
547 temp_buffers.push(b);
548 b
549 }
550 Err(_) => continue,
551 };
552
553 let material_uniforms = MaterialUniforms {
554 base_color,
555 emissive: emissive.with_alpha(specular_power),
556 ambient: khora_core::math::LinearRgba::new(0.1, 0.1, 0.1, 0.0),
557 };
558 let material_buffer = match device.create_buffer_with_data(
559 &BufferDescriptor {
560 label: None,
561 size: std::mem::size_of::<MaterialUniforms>() as u64,
562 usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
563 mapped_at_creation: false,
564 },
565 bytemuck::bytes_of(&material_uniforms),
566 ) {
567 Ok(b) => {
568 temp_buffers.push(b);
569 b
570 }
571 Err(_) => continue,
572 };
573
574 let mut model_bg = None;
576 if let Some(layout) = *self.model_layout.lock().unwrap() {
577 if let Ok(bg) = device.create_bind_group(&BindGroupDescriptor {
578 label: None,
579 layout,
580 entries: &[BindGroupEntry {
581 binding: 0,
582 resource: BindingResource::Buffer(BufferBinding {
583 buffer: model_buffer,
584 offset: 0,
585 size: None,
586 }),
587 _phantom: std::marker::PhantomData,
588 }],
589 }) {
590 model_bg = Some(bg);
591 temp_bind_groups.push(bg);
592 }
593 }
594
595 let mut material_bg = None;
596 if let Some(layout) = *self.material_layout.lock().unwrap() {
597 if let Ok(bg) = device.create_bind_group(&BindGroupDescriptor {
598 label: None,
599 layout,
600 entries: &[BindGroupEntry {
601 binding: 0,
602 resource: BindingResource::Buffer(BufferBinding {
603 buffer: material_buffer,
604 offset: 0,
605 size: None,
606 }),
607 _phantom: std::marker::PhantomData,
608 }],
609 }) {
610 material_bg = Some(bg);
611 temp_bind_groups.push(bg);
612 }
613 }
614
615 draw_commands.push(khora_core::renderer::api::command::DrawCommand {
616 pipeline: pipeline_id,
617 vertex_buffer: gpu_mesh_handle.vertex_buffer,
618 index_buffer: gpu_mesh_handle.index_buffer,
619 index_format: gpu_mesh_handle.index_format,
620 index_count: gpu_mesh_handle.index_count,
621 model_bind_group: model_bg,
622 model_offset: 0,
623 material_bind_group: material_bg,
624 material_offset: 0,
625 });
626 }
627 }
628
629 let color_attachment = RenderPassColorAttachment {
631 view: render_ctx.color_target,
632 resolve_target: None,
633 ops: Operations {
634 load: LoadOp::Clear(render_ctx.clear_color),
635 store: StoreOp::Store,
636 },
637 base_array_layer: 0,
638 };
639
640 let render_pass_desc = RenderPassDescriptor {
641 label: Some("Lit Forward Pass"),
642 color_attachments: &[color_attachment],
643 depth_stencil_attachment: render_ctx.depth_target.map(|depth_view| {
644 RenderPassDepthStencilAttachment {
645 view: depth_view,
646 depth_ops: Some(Operations {
647 load: LoadOp::Clear(1.0),
648 store: StoreOp::Store,
649 }),
650 stencil_ops: None,
651 base_array_layer: 0,
652 }
653 }),
654 };
655
656 let final_lighting_bind_group = if let Some(layout) = *self.light_layout.lock().unwrap() {
660 match (render_ctx.shadow_atlas, render_ctx.shadow_sampler) {
661 (Some(atlas), Some(sampler)) => {
662 let entries = [
663 BindGroupEntry {
664 binding: 0,
665 resource: BindingResource::Buffer(BufferBinding {
666 buffer: lighting_ring_buffer_id,
667 offset: 0,
668 size: None,
669 }),
670 _phantom: std::marker::PhantomData,
671 },
672 BindGroupEntry {
673 binding: 1,
674 resource: BindingResource::TextureView(*atlas),
675 _phantom: std::marker::PhantomData,
676 },
677 BindGroupEntry {
678 binding: 2,
679 resource: BindingResource::Sampler(*sampler),
680 _phantom: std::marker::PhantomData,
681 },
682 ];
683
684 match device.create_bind_group(&BindGroupDescriptor {
685 label: Some("lit_forward_lighting_bind_group_dynamic"),
686 layout,
687 entries: &entries,
688 }) {
689 Ok(bg) => {
690 temp_bind_groups.push(bg);
691 bg
692 }
693 Err(e) => {
694 log::error!(
695 "LitForwardLane: Failed to create lighting bind group: {:?}",
696 e
697 );
698 return;
699 }
700 }
701 }
702 _ => {
703 log::warn!(
704 "LitForwardLane: shadow atlas/sampler not available, skipping lit render"
705 );
706 return;
707 }
708 }
709 } else {
710 log::warn!("LitForwardLane: light layout not initialized");
711 return;
712 };
713
714 let mut render_pass = encoder.begin_render_pass(&render_pass_desc);
715
716 render_pass.set_bind_group(0, &camera_bind_group, &[]);
718 render_pass.set_bind_group(3, &final_lighting_bind_group, &[]);
719
720 let mut current_pipeline: Option<RenderPipelineId> = None;
721
722 for cmd in &draw_commands {
723 if current_pipeline != Some(pipeline_id) {
724 render_pass.set_pipeline(&pipeline_id);
725 current_pipeline = Some(pipeline_id);
726 }
727
728 if let Some(bg) = &cmd.model_bind_group {
729 render_pass.set_bind_group(1, bg, &[]);
730 }
731
732 if let Some(bg) = &cmd.material_bind_group {
733 render_pass.set_bind_group(2, bg, &[]);
734 }
735
736 render_pass.set_vertex_buffer(0, &cmd.vertex_buffer, 0);
737 render_pass.set_index_buffer(&cmd.index_buffer, 0, cmd.index_format);
738
739 render_pass.draw_indexed(0..cmd.index_count, 0, 0..1);
740 }
741
742 for bg in temp_bind_groups {
744 let _ = device.destroy_bind_group(bg);
745 }
746 for buf in temp_buffers {
747 let _ = device.destroy_buffer(buf);
748 }
749 }
750
751 fn estimate_render_cost(
752 &self,
753 render_world: &RenderWorld,
754 gpu_meshes: &RwLock<Assets<GpuMesh>>,
755 ) -> f32 {
756 let gpu_mesh_assets = gpu_meshes.read().unwrap();
757
758 let mut total_triangles = 0u32;
759 let mut draw_call_count = 0u32;
760
761 for extracted_mesh in &render_world.meshes {
762 if let Some(gpu_mesh) = gpu_mesh_assets.get(&extracted_mesh.cpu_mesh_uuid) {
763 let triangle_count = match gpu_mesh.primitive_topology {
765 PrimitiveTopology::TriangleList => gpu_mesh.index_count / 3,
766 PrimitiveTopology::TriangleStrip => {
767 if gpu_mesh.index_count >= 3 {
768 gpu_mesh.index_count - 2
769 } else {
770 0
771 }
772 }
773 PrimitiveTopology::LineList
774 | PrimitiveTopology::LineStrip
775 | PrimitiveTopology::PointList => 0,
776 };
777
778 total_triangles += triangle_count;
779 draw_call_count += 1;
780 }
781 }
782
783 let base_cost =
785 (total_triangles as f32 * TRIANGLE_COST) + (draw_call_count as f32 * DRAW_CALL_COST);
786
787 let shader_factor = self.shader_complexity.cost_multiplier();
789
790 let light_factor = self.light_cost_factor(render_world);
792
793 base_cost * shader_factor * light_factor
795 }
796
797 fn on_gpu_init(
798 &self,
799 device: &dyn khora_core::renderer::GraphicsDevice,
800 ) -> Result<(), khora_core::renderer::error::RenderError> {
801 use crate::render_lane::shaders::LIT_FORWARD_WGSL;
802 use khora_core::renderer::api::{
803 command::{
804 BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingType, BufferBindingType,
805 },
806 core::{ShaderModuleDescriptor, ShaderSourceData},
807 pipeline::enums::{CompareFunction, VertexFormat, VertexStepMode},
808 pipeline::state::{ColorWrites, DepthBiasState, StencilFaceState},
809 pipeline::{
810 ColorTargetStateDescriptor, DepthStencilStateDescriptor,
811 MultisampleStateDescriptor, PrimitiveStateDescriptor, RenderPipelineDescriptor,
812 VertexAttributeDescriptor, VertexBufferLayoutDescriptor,
813 },
814 util::{SampleCount, ShaderStageFlags, TextureFormat},
815 };
816 use std::borrow::Cow;
817
818 log::info!("LitForwardLane: Initializing GPU resources...");
819
820 let camera_layout = device
824 .create_bind_group_layout(&BindGroupLayoutDescriptor {
825 label: Some("lit_forward_camera_layout"),
826 entries: &[BindGroupLayoutEntry {
827 binding: 0,
828 visibility: ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
829 ty: BindingType::Buffer {
830 ty: BufferBindingType::Uniform,
831 has_dynamic_offset: false,
832 min_binding_size: None,
833 },
834 }],
835 })
836 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
837
838 let model_layout = device
840 .create_bind_group_layout(&BindGroupLayoutDescriptor {
841 label: Some("lit_forward_model_layout"),
842 entries: &[BindGroupLayoutEntry {
843 binding: 0,
844 visibility: ShaderStageFlags::VERTEX,
845 ty: BindingType::Buffer {
846 ty: BufferBindingType::Uniform,
847 has_dynamic_offset: false, min_binding_size: None,
849 },
850 }],
851 })
852 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
853
854 let material_layout = device
856 .create_bind_group_layout(&BindGroupLayoutDescriptor {
857 label: Some("lit_forward_material_layout"),
858 entries: &[BindGroupLayoutEntry {
859 binding: 0,
860 visibility: ShaderStageFlags::FRAGMENT,
861 ty: BindingType::Buffer {
862 ty: BufferBindingType::Uniform,
863 has_dynamic_offset: false,
864 min_binding_size: None,
865 },
866 }],
867 })
868 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
869
870 use khora_core::renderer::api::command::{SamplerBindingType, TextureSampleType};
872 let light_layout = device
873 .create_bind_group_layout(&BindGroupLayoutDescriptor {
874 label: Some("lit_forward_light_layout"),
875 entries: &[
876 BindGroupLayoutEntry {
877 binding: 0,
878 visibility: ShaderStageFlags::FRAGMENT,
879 ty: BindingType::Buffer {
880 ty: BufferBindingType::Uniform,
881 has_dynamic_offset: false,
882 min_binding_size: None,
883 },
884 },
885 BindGroupLayoutEntry {
886 binding: 1,
887 visibility: ShaderStageFlags::FRAGMENT,
888 ty: BindingType::Texture {
889 sample_type: TextureSampleType::Depth,
890 view_dimension:
891 khora_core::renderer::api::command::TextureViewDimension::D2Array,
892 multisampled: false,
893 },
894 },
895 BindGroupLayoutEntry {
896 binding: 2,
897 visibility: ShaderStageFlags::FRAGMENT,
898 ty: BindingType::Sampler(SamplerBindingType::Comparison),
899 },
900 ],
901 })
902 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
903
904 let shader_module = device
906 .create_shader_module(&ShaderModuleDescriptor {
907 label: Some("lit_forward_shader"),
908 source: ShaderSourceData::Wgsl(Cow::Borrowed(LIT_FORWARD_WGSL)),
909 })
910 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
911
912 let vertex_attributes = vec![
914 VertexAttributeDescriptor {
915 format: VertexFormat::Float32x3,
916 offset: 0,
917 shader_location: 0,
918 },
919 VertexAttributeDescriptor {
920 format: VertexFormat::Float32x3,
921 offset: 12,
922 shader_location: 1,
923 },
924 VertexAttributeDescriptor {
925 format: VertexFormat::Float32x2,
926 offset: 24,
927 shader_location: 2,
928 },
929 ];
930
931 let vertex_layout = VertexBufferLayoutDescriptor {
932 array_stride: 32, step_mode: VertexStepMode::Vertex,
934 attributes: Cow::Owned(vertex_attributes),
935 };
936
937 let pipeline_layout_ids = vec![camera_layout, model_layout, material_layout, light_layout];
938 *self.camera_layout.lock().unwrap() = Some(camera_layout);
940 *self.model_layout.lock().unwrap() = Some(model_layout);
941 *self.material_layout.lock().unwrap() = Some(material_layout);
942 *self.light_layout.lock().unwrap() = Some(light_layout);
943
944 let pipeline_layout_desc = khora_core::renderer::api::pipeline::PipelineLayoutDescriptor {
946 label: Some(Cow::Borrowed("LitForward Pipeline Layout")),
947 bind_group_layouts: &pipeline_layout_ids,
948 };
949
950 let pipeline_layout_id = device
951 .create_pipeline_layout(&pipeline_layout_desc)
952 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
953
954 let pipeline_desc = RenderPipelineDescriptor {
955 label: Some(Cow::Borrowed("LitForward Pipeline")),
956 layout: Some(pipeline_layout_id),
957 vertex_shader_module: shader_module,
958 vertex_entry_point: Cow::Borrowed("vs_main"),
959 fragment_shader_module: Some(shader_module),
960 fragment_entry_point: Some(Cow::Borrowed("fs_main")),
961 vertex_buffers_layout: Cow::Owned(vec![vertex_layout]),
962 primitive_state: PrimitiveStateDescriptor {
963 topology: PrimitiveTopology::TriangleList,
964 ..Default::default()
965 },
966 depth_stencil_state: Some(DepthStencilStateDescriptor {
967 format: TextureFormat::Depth32Float,
968 depth_write_enabled: true,
969 depth_compare: CompareFunction::Less,
970 stencil_front: StencilFaceState::default(),
971 stencil_back: StencilFaceState::default(),
972 stencil_read_mask: 0,
973 stencil_write_mask: 0,
974 bias: DepthBiasState::default(),
975 }),
976 color_target_states: Cow::Owned(vec![ColorTargetStateDescriptor {
977 format: device
978 .get_surface_format()
979 .unwrap_or(TextureFormat::Rgba8UnormSrgb),
980 blend: None,
981 write_mask: ColorWrites::ALL,
982 }]),
983 multisample_state: MultisampleStateDescriptor {
984 count: SampleCount::X1,
985 mask: !0,
986 alpha_to_coverage_enabled: false,
987 },
988 };
989
990 let pipeline_id = device
991 .create_render_pipeline(&pipeline_desc)
992 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
993
994 let mut pipeline_lock = self.pipeline.lock().unwrap();
995 *pipeline_lock = Some(pipeline_id);
996
997 let camera_ring = UniformRingBuffer::new(
1001 device,
1002 camera_layout,
1003 0,
1004 std::mem::size_of::<khora_core::renderer::api::resource::CameraUniformData>() as u64,
1005 "Camera Uniform Ring",
1006 )
1007 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
1008
1009 let lighting_buffer_layout = device
1013 .create_bind_group_layout(&BindGroupLayoutDescriptor {
1014 label: Some("lit_forward_lighting_buffer_layout"),
1015 entries: &[BindGroupLayoutEntry {
1016 binding: 0,
1017 visibility: ShaderStageFlags::FRAGMENT,
1018 ty: BindingType::Buffer {
1019 ty: BufferBindingType::Uniform,
1020 has_dynamic_offset: false,
1021 min_binding_size: None,
1022 },
1023 }],
1024 })
1025 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
1026
1027 *self.lighting_buffer_layout.lock().unwrap() = Some(lighting_buffer_layout);
1028
1029 let lighting_ring = UniformRingBuffer::new(
1030 device,
1031 lighting_buffer_layout,
1032 0,
1033 std::mem::size_of::<LightingUniforms>() as u64,
1034 "Lighting Uniform Ring",
1035 )
1036 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
1037
1038 *self.camera_ring.lock().unwrap() = Some(camera_ring);
1039 *self.lighting_ring.lock().unwrap() = Some(lighting_ring);
1040
1041 log::info!(
1042 "LitForwardLane: Persistent ring buffers created (camera: {} bytes, lighting: {} bytes, {} slots each)",
1043 std::mem::size_of::<khora_core::renderer::api::resource::CameraUniformData>(),
1044 std::mem::size_of::<LightingUniforms>(),
1045 khora_core::renderer::api::core::MAX_FRAMES_IN_FLIGHT,
1046 );
1047
1048 Ok(())
1049 }
1050
1051 fn on_gpu_shutdown(&self, device: &dyn khora_core::renderer::GraphicsDevice) {
1052 if let Some(ring) = self.camera_ring.lock().unwrap().take() {
1054 ring.destroy(device);
1055 }
1056 if let Some(ring) = self.lighting_ring.lock().unwrap().take() {
1057 ring.destroy(device);
1058 }
1059
1060 let mut pipeline_lock = self.pipeline.lock().unwrap();
1061 if let Some(id) = pipeline_lock.take() {
1062 let _ = device.destroy_render_pipeline(id);
1063 }
1064 if let Some(id) = self.camera_layout.lock().unwrap().take() {
1065 let _ = device.destroy_bind_group_layout(id);
1066 }
1067 if let Some(id) = self.model_layout.lock().unwrap().take() {
1068 let _ = device.destroy_bind_group_layout(id);
1069 }
1070 if let Some(id) = self.material_layout.lock().unwrap().take() {
1071 let _ = device.destroy_bind_group_layout(id);
1072 }
1073 if let Some(id) = self.light_layout.lock().unwrap().take() {
1074 let _ = device.destroy_bind_group_layout(id);
1075 }
1076 if let Some(id) = self.lighting_buffer_layout.lock().unwrap().take() {
1077 let _ = device.destroy_bind_group_layout(id);
1078 }
1079 }
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 use super::*;
1085 use crate::render_lane::world::ExtractedMesh;
1086 use khora_core::lane::Lane;
1087 use khora_core::{
1088 asset::{AssetHandle, AssetUUID},
1089 math::{affine_transform::AffineTransform, Mat4},
1090 renderer::{
1091 api::{pipeline::enums::PrimitiveTopology, resource::BufferId, util::IndexFormat},
1092 light::DirectionalLight,
1093 },
1094 };
1095 use std::sync::Arc;
1096
1097 fn create_test_gpu_mesh(index_count: u32) -> GpuMesh {
1098 GpuMesh {
1099 vertex_buffer: BufferId(0),
1100 index_buffer: BufferId(1),
1101 index_count,
1102 index_format: IndexFormat::Uint32,
1103 primitive_topology: PrimitiveTopology::TriangleList,
1104 }
1105 }
1106
1107 #[test]
1108 fn test_lit_forward_lane_creation() {
1109 let lane = LitForwardLane::new();
1110 assert_eq!(lane.strategy_name(), "LitForward");
1111 assert_eq!(lane.shader_complexity, ShaderComplexity::SimpleLit);
1112 }
1113
1114 #[test]
1115 fn test_lit_forward_lane_with_complexity() {
1116 let lane = LitForwardLane::with_complexity(ShaderComplexity::FullPBR);
1117 assert_eq!(lane.shader_complexity, ShaderComplexity::FullPBR);
1118 }
1119
1120 #[test]
1121 fn test_shader_complexity_ordering() {
1122 assert!(ShaderComplexity::Unlit < ShaderComplexity::SimpleLit);
1123 assert!(ShaderComplexity::SimpleLit < ShaderComplexity::FullPBR);
1124 }
1125
1126 #[test]
1127 fn test_shader_complexity_cost_multipliers() {
1128 assert_eq!(ShaderComplexity::Unlit.cost_multiplier(), 1.0);
1129 assert_eq!(ShaderComplexity::SimpleLit.cost_multiplier(), 1.5);
1130 assert_eq!(ShaderComplexity::FullPBR.cost_multiplier(), 2.5);
1131 }
1132
1133 #[test]
1134 fn test_cost_estimation_empty_world() {
1135 let lane = LitForwardLane::new();
1136 let render_world = RenderWorld::default();
1137 let gpu_meshes = Arc::new(RwLock::new(Assets::<GpuMesh>::new()));
1138
1139 let cost = lane.estimate_render_cost(&render_world, &gpu_meshes);
1140 assert_eq!(cost, 0.0, "Empty world should have zero cost");
1141 }
1142
1143 #[test]
1144 fn test_cost_estimation_with_meshes() {
1145 let lane = LitForwardLane::new();
1146
1147 let mesh_uuid = AssetUUID::new();
1149 let gpu_mesh = create_test_gpu_mesh(300);
1150
1151 let mut gpu_meshes = Assets::<GpuMesh>::new();
1152 gpu_meshes.insert(mesh_uuid, AssetHandle::new(gpu_mesh));
1153
1154 let mut render_world = RenderWorld::default();
1155 render_world.meshes.push(ExtractedMesh {
1156 transform: AffineTransform::default(),
1157 cpu_mesh_uuid: mesh_uuid,
1158 gpu_mesh: AssetHandle::new(create_test_gpu_mesh(300)),
1159 material: None,
1160 });
1161
1162 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
1163 let cost = lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
1164
1165 assert!(
1169 (cost - 0.3).abs() < 0.0001,
1170 "Cost should be 0.3 for 100 triangles with SimpleLit complexity, got {}",
1171 cost
1172 );
1173 }
1174
1175 #[test]
1176 fn test_cost_estimation_with_lights() {
1177 use crate::render_lane::world::ExtractedLight;
1178 use khora_core::{
1179 math::{Mat4, Vec3},
1180 renderer::light::LightType,
1181 };
1182
1183 let lane = LitForwardLane::new();
1184
1185 let mesh_uuid = AssetUUID::new();
1187 let gpu_mesh = create_test_gpu_mesh(300);
1188
1189 let mut gpu_meshes = Assets::<GpuMesh>::new();
1190 gpu_meshes.insert(mesh_uuid, AssetHandle::new(gpu_mesh));
1191
1192 let mut render_world = RenderWorld::default();
1193 render_world.meshes.push(ExtractedMesh {
1194 transform: AffineTransform::default(),
1195 cpu_mesh_uuid: mesh_uuid,
1196 gpu_mesh: AssetHandle::new(create_test_gpu_mesh(300)),
1197 material: None,
1198 });
1199
1200 for _ in 0..4 {
1202 render_world.lights.push(ExtractedLight {
1203 light_type: LightType::Directional(DirectionalLight::default()),
1204 position: Vec3::ZERO,
1205 direction: Vec3::new(0.0, -1.0, 0.0),
1206 shadow_view_proj: Mat4::IDENTITY,
1207 shadow_atlas_index: None,
1208 });
1209 }
1210
1211 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
1212 let cost = lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
1213
1214 assert!(
1219 (cost - 0.36).abs() < 0.0001,
1220 "Cost should be 0.36 with 4 lights, got {}",
1221 cost
1222 );
1223 }
1224
1225 #[test]
1226 fn test_cost_increases_with_complexity() {
1227 let mesh_uuid = AssetUUID::new();
1228 let gpu_mesh = create_test_gpu_mesh(300);
1229
1230 let mut gpu_meshes = Assets::<GpuMesh>::new();
1231 gpu_meshes.insert(mesh_uuid, AssetHandle::new(gpu_mesh));
1232
1233 let mut render_world = RenderWorld::default();
1234 render_world.meshes.push(ExtractedMesh {
1235 transform: AffineTransform::default(),
1236 cpu_mesh_uuid: mesh_uuid,
1237 gpu_mesh: AssetHandle::new(create_test_gpu_mesh(300)),
1238 material: None,
1239 });
1240
1241 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
1242
1243 let unlit_lane = LitForwardLane::with_complexity(ShaderComplexity::Unlit);
1244 let simple_lane = LitForwardLane::with_complexity(ShaderComplexity::SimpleLit);
1245 let pbr_lane = LitForwardLane::with_complexity(ShaderComplexity::FullPBR);
1246
1247 let unlit_cost = unlit_lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
1248 let simple_cost = simple_lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
1249 let pbr_cost = pbr_lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
1250
1251 assert!(
1252 unlit_cost < simple_cost,
1253 "Unlit should be cheaper than SimpleLit"
1254 );
1255 assert!(
1256 simple_cost < pbr_cost,
1257 "SimpleLit should be cheaper than PBR"
1258 );
1259 }
1260
1261 #[test]
1262 fn test_effective_light_counts() {
1263 use crate::render_lane::world::ExtractedLight;
1264 use khora_core::{
1265 math::Vec3,
1266 renderer::light::{LightType, PointLight},
1267 };
1268
1269 let lane = LitForwardLane {
1270 max_directional_lights: 2,
1271 max_point_lights: 4,
1272 max_spot_lights: 2,
1273 ..Default::default()
1274 };
1275
1276 let mut render_world = RenderWorld::default();
1277
1278 for _ in 0..5 {
1280 render_world.lights.push(ExtractedLight {
1281 light_type: LightType::Directional(DirectionalLight::default()),
1282 position: Vec3::ZERO,
1283 direction: Vec3::new(0.0, -1.0, 0.0),
1284 shadow_view_proj: Mat4::IDENTITY,
1285 shadow_atlas_index: None,
1286 });
1287 }
1288
1289 for _ in 0..3 {
1291 render_world.lights.push(ExtractedLight {
1292 light_type: LightType::Point(PointLight::default()),
1293 position: Vec3::ZERO,
1294 direction: Vec3::ZERO,
1295 shadow_view_proj: Mat4::IDENTITY,
1296 shadow_atlas_index: None,
1297 });
1298 }
1299
1300 let (dir, point, spot) = lane.effective_light_counts(&render_world);
1301 assert_eq!(dir, 2, "Should be clamped to max 2 directional lights");
1302 assert_eq!(point, 3, "Should use all 3 point lights (under max)");
1303 assert_eq!(spot, 0, "Should have 0 spot lights");
1304 }
1305
1306 #[test]
1307 fn test_get_pipeline_for_material() {
1308 let lane = LitForwardLane::new();
1309
1310 let pipeline = lane.get_pipeline_for_material(None);
1312 assert_eq!(pipeline, RenderPipelineId(0));
1313
1314 let pipeline = lane.get_pipeline_for_material(None);
1316 assert_eq!(pipeline, RenderPipelineId(0));
1317 }
1318}