1use crate::render_lane::RenderLane;
28
29use super::RenderWorld;
30use khora_core::{
31 asset::{AssetUUID, Material},
32 renderer::{
33 api::{
34 command::{
35 LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor, StoreOp,
36 },
37 PrimitiveTopology,
38 },
39 traits::CommandEncoder,
40 GpuMesh, RenderContext, RenderPipelineId,
41 },
42};
43use khora_data::assets::Assets;
44use std::sync::RwLock;
45
46const TRIANGLE_COST: f32 = 0.001;
48const DRAW_CALL_COST: f32 = 0.1;
49const LIGHT_COST_FACTOR: f32 = 0.05;
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
58pub enum ShaderComplexity {
59 Unlit,
62 #[default]
65 SimpleLit,
66 FullPBR,
69}
70
71impl ShaderComplexity {
72 pub fn cost_multiplier(&self) -> f32 {
77 match self {
78 ShaderComplexity::Unlit => 1.0,
79 ShaderComplexity::SimpleLit => 1.5,
80 ShaderComplexity::FullPBR => 2.5,
81 }
82 }
83
84 pub fn name(&self) -> &'static str {
86 match self {
87 ShaderComplexity::Unlit => "Unlit",
88 ShaderComplexity::SimpleLit => "SimpleLit",
89 ShaderComplexity::FullPBR => "FullPBR",
90 }
91 }
92}
93
94#[derive(Debug)]
113pub struct LitForwardLane {
114 pub shader_complexity: ShaderComplexity,
116 pub max_directional_lights: u32,
118 pub max_point_lights: u32,
120 pub max_spot_lights: u32,
122}
123
124impl Default for LitForwardLane {
125 fn default() -> Self {
126 Self {
127 shader_complexity: ShaderComplexity::SimpleLit,
128 max_directional_lights: 4,
129 max_point_lights: 16,
130 max_spot_lights: 8,
131 }
132 }
133}
134
135impl LitForwardLane {
136 pub fn new() -> Self {
138 Self::default()
139 }
140
141 pub fn with_complexity(complexity: ShaderComplexity) -> Self {
143 Self {
144 shader_complexity: complexity,
145 ..Default::default()
146 }
147 }
148
149 pub fn effective_light_counts(&self, render_world: &RenderWorld) -> (usize, usize, usize) {
153 let dir_count = render_world
154 .directional_light_count()
155 .min(self.max_directional_lights as usize);
156 let point_count = render_world
157 .point_light_count()
158 .min(self.max_point_lights as usize);
159 let spot_count = render_world
160 .spot_light_count()
161 .min(self.max_spot_lights as usize);
162
163 (dir_count, point_count, spot_count)
164 }
165
166 fn light_cost_factor(&self, render_world: &RenderWorld) -> f32 {
168 let (dir_count, point_count, spot_count) = self.effective_light_counts(render_world);
169 let total_lights = dir_count + point_count + spot_count;
170
171 1.0 + (total_lights as f32 * LIGHT_COST_FACTOR)
173 }
174}
175
176impl RenderLane for LitForwardLane {
177 fn strategy_name(&self) -> &'static str {
178 "LitForward"
179 }
180
181 fn get_pipeline_for_material(
182 &self,
183 material_uuid: Option<AssetUUID>,
184 materials: &Assets<Box<dyn Material>>,
185 ) -> RenderPipelineId {
186 if let Some(uuid) = material_uuid {
188 if materials.get(&uuid).is_none() {
189 let _ = uuid;
191 }
192 }
193
194 RenderPipelineId(1)
198 }
199
200 fn render(
201 &self,
202 render_world: &RenderWorld,
203 encoder: &mut dyn CommandEncoder,
204 render_ctx: &RenderContext,
205 gpu_meshes: &RwLock<Assets<GpuMesh>>,
206 materials: &RwLock<Assets<Box<dyn Material>>>,
207 ) {
208 let gpu_mesh_assets = gpu_meshes.read().unwrap();
210 let material_assets = materials.read().unwrap();
211
212 let pipelines: Vec<RenderPipelineId> = render_world
214 .meshes
215 .iter()
216 .map(|mesh| self.get_pipeline_for_material(mesh.material_uuid, &material_assets))
217 .collect();
218
219 let color_attachment = RenderPassColorAttachment {
221 view: render_ctx.color_target,
222 resolve_target: None,
223 ops: Operations {
224 load: LoadOp::Clear(render_ctx.clear_color),
225 store: StoreOp::Store,
226 },
227 };
228
229 let render_pass_desc = RenderPassDescriptor {
230 label: Some("Lit Forward Pass"),
231 color_attachments: &[color_attachment],
232 };
233
234 let mut render_pass = encoder.begin_render_pass(&render_pass_desc);
236
237 let mut current_pipeline: Option<RenderPipelineId> = None;
239
240 for (i, extracted_mesh) in render_world.meshes.iter().enumerate() {
246 if let Some(gpu_mesh_handle) = gpu_mesh_assets.get(&extracted_mesh.gpu_mesh_uuid) {
247 let pipeline = &pipelines[i];
248
249 if current_pipeline != Some(*pipeline) {
251 render_pass.set_pipeline(pipeline);
252 current_pipeline = Some(*pipeline);
253 }
254
255 render_pass.set_vertex_buffer(0, &gpu_mesh_handle.vertex_buffer, 0);
257
258 render_pass.set_index_buffer(
260 &gpu_mesh_handle.index_buffer,
261 0,
262 gpu_mesh_handle.index_format,
263 );
264
265 render_pass.draw_indexed(0..gpu_mesh_handle.index_count, 0, 0..1);
267 }
268 }
269 }
270
271 fn estimate_cost(
272 &self,
273 render_world: &RenderWorld,
274 gpu_meshes: &RwLock<Assets<GpuMesh>>,
275 ) -> f32 {
276 let gpu_mesh_assets = gpu_meshes.read().unwrap();
277
278 let mut total_triangles = 0u32;
279 let mut draw_call_count = 0u32;
280
281 for extracted_mesh in &render_world.meshes {
282 if let Some(gpu_mesh) = gpu_mesh_assets.get(&extracted_mesh.gpu_mesh_uuid) {
283 let triangle_count = match gpu_mesh.primitive_topology {
285 PrimitiveTopology::TriangleList => gpu_mesh.index_count / 3,
286 PrimitiveTopology::TriangleStrip => {
287 if gpu_mesh.index_count >= 3 {
288 gpu_mesh.index_count - 2
289 } else {
290 0
291 }
292 }
293 PrimitiveTopology::LineList
294 | PrimitiveTopology::LineStrip
295 | PrimitiveTopology::PointList => 0,
296 };
297
298 total_triangles += triangle_count;
299 draw_call_count += 1;
300 }
301 }
302
303 let base_cost =
305 (total_triangles as f32 * TRIANGLE_COST) + (draw_call_count as f32 * DRAW_CALL_COST);
306
307 let shader_factor = self.shader_complexity.cost_multiplier();
309
310 let light_factor = self.light_cost_factor(render_world);
312
313 base_cost * shader_factor * light_factor
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use crate::render_lane::world::ExtractedMesh;
322 use khora_core::{
323 asset::{AssetHandle, AssetUUID},
324 math::affine_transform::AffineTransform,
325 renderer::{api::PrimitiveTopology, light::DirectionalLight, BufferId, IndexFormat},
326 };
327 use std::sync::Arc;
328
329 fn create_test_gpu_mesh(index_count: u32) -> GpuMesh {
330 GpuMesh {
331 vertex_buffer: BufferId(0),
332 index_buffer: BufferId(1),
333 index_count,
334 index_format: IndexFormat::Uint32,
335 primitive_topology: PrimitiveTopology::TriangleList,
336 }
337 }
338
339 #[test]
340 fn test_lit_forward_lane_creation() {
341 let lane = LitForwardLane::new();
342 assert_eq!(lane.strategy_name(), "LitForward");
343 assert_eq!(lane.shader_complexity, ShaderComplexity::SimpleLit);
344 }
345
346 #[test]
347 fn test_lit_forward_lane_with_complexity() {
348 let lane = LitForwardLane::with_complexity(ShaderComplexity::FullPBR);
349 assert_eq!(lane.shader_complexity, ShaderComplexity::FullPBR);
350 }
351
352 #[test]
353 fn test_shader_complexity_ordering() {
354 assert!(ShaderComplexity::Unlit < ShaderComplexity::SimpleLit);
355 assert!(ShaderComplexity::SimpleLit < ShaderComplexity::FullPBR);
356 }
357
358 #[test]
359 fn test_shader_complexity_cost_multipliers() {
360 assert_eq!(ShaderComplexity::Unlit.cost_multiplier(), 1.0);
361 assert_eq!(ShaderComplexity::SimpleLit.cost_multiplier(), 1.5);
362 assert_eq!(ShaderComplexity::FullPBR.cost_multiplier(), 2.5);
363 }
364
365 #[test]
366 fn test_cost_estimation_empty_world() {
367 let lane = LitForwardLane::new();
368 let render_world = RenderWorld::default();
369 let gpu_meshes = Arc::new(RwLock::new(Assets::<GpuMesh>::new()));
370
371 let cost = lane.estimate_cost(&render_world, &gpu_meshes);
372 assert_eq!(cost, 0.0, "Empty world should have zero cost");
373 }
374
375 #[test]
376 fn test_cost_estimation_with_meshes() {
377 let lane = LitForwardLane::new();
378
379 let mesh_uuid = AssetUUID::new();
381 let gpu_mesh = create_test_gpu_mesh(300);
382
383 let mut gpu_meshes = Assets::<GpuMesh>::new();
384 gpu_meshes.insert(mesh_uuid, AssetHandle::new(gpu_mesh));
385
386 let mut render_world = RenderWorld::default();
387 render_world.meshes.push(ExtractedMesh {
388 transform: AffineTransform::default(),
389 gpu_mesh_uuid: mesh_uuid,
390 material_uuid: None,
391 });
392
393 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
394 let cost = lane.estimate_cost(&render_world, &gpu_meshes_lock);
395
396 assert!(
400 (cost - 0.3).abs() < 0.0001,
401 "Cost should be 0.3 for 100 triangles with SimpleLit complexity, got {}",
402 cost
403 );
404 }
405
406 #[test]
407 fn test_cost_estimation_with_lights() {
408 use crate::render_lane::world::ExtractedLight;
409 use khora_core::{math::Vec3, renderer::light::LightType};
410
411 let lane = LitForwardLane::new();
412
413 let mesh_uuid = AssetUUID::new();
415 let gpu_mesh = create_test_gpu_mesh(300);
416
417 let mut gpu_meshes = Assets::<GpuMesh>::new();
418 gpu_meshes.insert(mesh_uuid, AssetHandle::new(gpu_mesh));
419
420 let mut render_world = RenderWorld::default();
421 render_world.meshes.push(ExtractedMesh {
422 transform: AffineTransform::default(),
423 gpu_mesh_uuid: mesh_uuid,
424 material_uuid: None,
425 });
426
427 for _ in 0..4 {
429 render_world.lights.push(ExtractedLight {
430 light_type: LightType::Directional(DirectionalLight::default()),
431 position: Vec3::ZERO,
432 direction: Vec3::new(0.0, -1.0, 0.0),
433 });
434 }
435
436 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
437 let cost = lane.estimate_cost(&render_world, &gpu_meshes_lock);
438
439 assert!(
444 (cost - 0.36).abs() < 0.0001,
445 "Cost should be 0.36 with 4 lights, got {}",
446 cost
447 );
448 }
449
450 #[test]
451 fn test_cost_increases_with_complexity() {
452 let mesh_uuid = AssetUUID::new();
453 let gpu_mesh = create_test_gpu_mesh(300);
454
455 let mut gpu_meshes = Assets::<GpuMesh>::new();
456 gpu_meshes.insert(mesh_uuid, AssetHandle::new(gpu_mesh));
457
458 let mut render_world = RenderWorld::default();
459 render_world.meshes.push(ExtractedMesh {
460 transform: AffineTransform::default(),
461 gpu_mesh_uuid: mesh_uuid,
462 material_uuid: None,
463 });
464
465 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
466
467 let unlit_lane = LitForwardLane::with_complexity(ShaderComplexity::Unlit);
468 let simple_lane = LitForwardLane::with_complexity(ShaderComplexity::SimpleLit);
469 let pbr_lane = LitForwardLane::with_complexity(ShaderComplexity::FullPBR);
470
471 let unlit_cost = unlit_lane.estimate_cost(&render_world, &gpu_meshes_lock);
472 let simple_cost = simple_lane.estimate_cost(&render_world, &gpu_meshes_lock);
473 let pbr_cost = pbr_lane.estimate_cost(&render_world, &gpu_meshes_lock);
474
475 assert!(
476 unlit_cost < simple_cost,
477 "Unlit should be cheaper than SimpleLit"
478 );
479 assert!(
480 simple_cost < pbr_cost,
481 "SimpleLit should be cheaper than PBR"
482 );
483 }
484
485 #[test]
486 fn test_effective_light_counts() {
487 use crate::render_lane::world::ExtractedLight;
488 use khora_core::{
489 math::Vec3,
490 renderer::light::{LightType, PointLight},
491 };
492
493 let lane = LitForwardLane {
494 max_directional_lights: 2,
495 max_point_lights: 4,
496 max_spot_lights: 2,
497 ..Default::default()
498 };
499
500 let mut render_world = RenderWorld::default();
501
502 for _ in 0..5 {
504 render_world.lights.push(ExtractedLight {
505 light_type: LightType::Directional(DirectionalLight::default()),
506 position: Vec3::ZERO,
507 direction: Vec3::new(0.0, -1.0, 0.0),
508 });
509 }
510
511 for _ in 0..3 {
513 render_world.lights.push(ExtractedLight {
514 light_type: LightType::Point(PointLight::default()),
515 position: Vec3::ZERO,
516 direction: Vec3::ZERO,
517 });
518 }
519
520 let (dir, point, spot) = lane.effective_light_counts(&render_world);
521 assert_eq!(dir, 2, "Should be clamped to max 2 directional lights");
522 assert_eq!(point, 3, "Should use all 3 point lights (under max)");
523 assert_eq!(spot, 0, "Should have 0 spot lights");
524 }
525
526 #[test]
527 fn test_get_pipeline_for_material() {
528 let lane = LitForwardLane::new();
529 let materials = Assets::<Box<dyn Material>>::new();
530
531 let pipeline = lane.get_pipeline_for_material(None, &materials);
533 assert_eq!(pipeline, RenderPipelineId(1));
534
535 let pipeline = lane.get_pipeline_for_material(Some(AssetUUID::new()), &materials);
537 assert_eq!(pipeline, RenderPipelineId(1));
538 }
539}