1use crate::render_lane::RenderLane;
30
31use super::RenderWorld;
32use khora_core::{
33 asset::{AssetUUID, Material},
34 renderer::{
35 api::{
36 command::{
37 LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor, StoreOp,
38 },
39 PrimitiveTopology,
40 },
41 traits::CommandEncoder,
42 GpuMesh, RenderContext, RenderPipelineId,
43 },
44};
45use khora_data::assets::Assets;
46use std::sync::RwLock;
47
48#[derive(Default)]
60pub struct SimpleUnlitLane;
61
62impl SimpleUnlitLane {
63 pub fn new() -> Self {
67 Self
68 }
69}
70
71impl RenderLane for SimpleUnlitLane {
72 fn strategy_name(&self) -> &'static str {
73 "SimpleUnlit"
74 }
75
76 fn get_pipeline_for_material(
77 &self,
78 material_uuid: Option<AssetUUID>,
79 materials: &Assets<Box<dyn Material>>,
80 ) -> RenderPipelineId {
81 if let Some(uuid) = material_uuid {
83 if materials.get(&uuid).is_none() {
84 let _ = uuid; }
87 }
88
89 RenderPipelineId(0)
95 }
96
97 fn render(
98 &self,
99 render_world: &RenderWorld,
100 encoder: &mut dyn CommandEncoder,
101 render_ctx: &RenderContext,
102 gpu_meshes: &RwLock<Assets<GpuMesh>>,
103 materials: &RwLock<Assets<Box<dyn Material>>>,
104 ) {
105 let gpu_mesh_assets = gpu_meshes.read().unwrap();
107 let material_assets = materials.read().unwrap();
108
109 let pipelines: Vec<RenderPipelineId> = render_world
112 .meshes
113 .iter()
114 .map(|mesh| self.get_pipeline_for_material(mesh.material_uuid, &material_assets))
115 .collect();
116
117 let color_attachment = RenderPassColorAttachment {
119 view: render_ctx.color_target,
120 resolve_target: None,
121 ops: Operations {
122 load: LoadOp::Clear(render_ctx.clear_color),
123 store: StoreOp::Store,
124 },
125 };
126
127 let render_pass_desc = RenderPassDescriptor {
128 label: Some("Simple Unlit Pass"),
129 color_attachments: &[color_attachment],
130 };
131
132 let mut render_pass = encoder.begin_render_pass(&render_pass_desc);
134
135 let mut current_pipeline: Option<RenderPipelineId> = None;
137
138 for (i, extracted_mesh) in render_world.meshes.iter().enumerate() {
140 if let Some(gpu_mesh_handle) = gpu_mesh_assets.get(&extracted_mesh.gpu_mesh_uuid) {
142 let pipeline = &pipelines[i];
144
145 if current_pipeline != Some(*pipeline) {
148 render_pass.set_pipeline(pipeline);
149 current_pipeline = Some(*pipeline);
150 }
151
152 render_pass.set_vertex_buffer(0, &gpu_mesh_handle.vertex_buffer, 0);
154
155 render_pass.set_index_buffer(
157 &gpu_mesh_handle.index_buffer,
158 0,
159 gpu_mesh_handle.index_format,
160 );
161
162 render_pass.draw_indexed(0..gpu_mesh_handle.index_count, 0, 0..1);
164 }
165 }
166 }
167
168 fn estimate_cost(
169 &self,
170 render_world: &RenderWorld,
171 gpu_meshes: &RwLock<Assets<GpuMesh>>,
172 ) -> f32 {
173 let gpu_mesh_assets = gpu_meshes.read().unwrap();
174
175 let mut total_triangles = 0u32;
176 let mut draw_call_count = 0u32;
177
178 for extracted_mesh in &render_world.meshes {
179 if let Some(gpu_mesh) = gpu_mesh_assets.get(&extracted_mesh.gpu_mesh_uuid) {
180 let triangle_count = match gpu_mesh.primitive_topology {
182 PrimitiveTopology::TriangleList => gpu_mesh.index_count / 3,
183 PrimitiveTopology::TriangleStrip => {
184 if gpu_mesh.index_count >= 3 {
185 gpu_mesh.index_count - 2
186 } else {
187 0
188 }
189 }
190 PrimitiveTopology::LineList
192 | PrimitiveTopology::LineStrip
193 | PrimitiveTopology::PointList => 0,
194 };
195
196 total_triangles += triangle_count;
197 draw_call_count += 1;
198 }
199 }
200
201 const TRIANGLE_COST: f32 = 0.001;
204 const DRAW_CALL_COST: f32 = 0.1;
205
206 (total_triangles as f32 * TRIANGLE_COST) + (draw_call_count as f32 * DRAW_CALL_COST)
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use khora_core::{
214 asset::AssetHandle,
215 renderer::{api::PrimitiveTopology, BufferId, IndexFormat},
216 };
217 use std::sync::Arc;
218
219 #[test]
220 fn test_simple_unlit_lane_creation() {
221 let lane = SimpleUnlitLane::new();
222 assert_eq!(lane.strategy_name(), "SimpleUnlit");
223 }
224
225 #[test]
226 fn test_default_construction() {
227 let lane = SimpleUnlitLane;
228 assert_eq!(lane.strategy_name(), "SimpleUnlit");
229 }
230
231 #[test]
232 fn test_cost_estimation_empty_world() {
233 let lane = SimpleUnlitLane::new();
234 let render_world = RenderWorld::default();
235 let gpu_meshes = Arc::new(RwLock::new(Assets::<GpuMesh>::new()));
236
237 let cost = lane.estimate_cost(&render_world, &gpu_meshes);
238 assert_eq!(cost, 0.0, "Empty world should have zero cost");
239 }
240
241 #[test]
242 fn test_cost_estimation_triangle_list() {
243 use crate::render_lane::world::ExtractedMesh;
244 use khora_core::asset::AssetUUID;
245
246 let lane = SimpleUnlitLane::new();
247
248 let mesh_uuid = AssetUUID::new();
250 let gpu_mesh = GpuMesh {
251 vertex_buffer: BufferId(0),
252 index_buffer: BufferId(1),
253 index_count: 300,
254 index_format: IndexFormat::Uint32,
255 primitive_topology: PrimitiveTopology::TriangleList,
256 };
257
258 let mut gpu_meshes = Assets::<GpuMesh>::new();
259 gpu_meshes.insert(mesh_uuid, AssetHandle::new(gpu_mesh));
260
261 let mut render_world = RenderWorld::default();
262 render_world.meshes.push(ExtractedMesh {
263 transform: Default::default(),
264 gpu_mesh_uuid: mesh_uuid,
265 material_uuid: None,
266 });
267
268 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
269 let cost = lane.estimate_cost(&render_world, &gpu_meshes_lock);
270
271 assert_eq!(
273 cost, 0.2,
274 "Cost should be 0.2 for 100 triangles + 1 draw call"
275 );
276 }
277
278 #[test]
279 fn test_cost_estimation_triangle_strip() {
280 use crate::render_lane::world::ExtractedMesh;
281 use khora_core::asset::AssetUUID;
282
283 let lane = SimpleUnlitLane::new();
284
285 let mesh_uuid = AssetUUID::new();
287 let gpu_mesh = GpuMesh {
288 vertex_buffer: BufferId(0),
289 index_buffer: BufferId(1),
290 index_count: 52,
291 index_format: IndexFormat::Uint16,
292 primitive_topology: PrimitiveTopology::TriangleStrip,
293 };
294
295 let mut gpu_meshes = Assets::<GpuMesh>::new();
296 gpu_meshes.insert(mesh_uuid, AssetHandle::new(gpu_mesh));
297
298 let mut render_world = RenderWorld::default();
299 render_world.meshes.push(ExtractedMesh {
300 transform: Default::default(),
301 gpu_mesh_uuid: mesh_uuid,
302 material_uuid: None,
303 });
304
305 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
306 let cost = lane.estimate_cost(&render_world, &gpu_meshes_lock);
307
308 assert_eq!(
310 cost, 0.15,
311 "Cost should be 0.15 for 50 triangles + 1 draw call"
312 );
313 }
314
315 #[test]
316 fn test_cost_estimation_lines_and_points() {
317 use crate::render_lane::world::ExtractedMesh;
318 use khora_core::asset::AssetUUID;
319
320 let lane = SimpleUnlitLane::new();
321
322 let line_uuid = AssetUUID::new();
324 let point_uuid = AssetUUID::new();
325
326 let line_mesh = GpuMesh {
327 vertex_buffer: BufferId(0),
328 index_buffer: BufferId(1),
329 index_count: 100,
330 index_format: IndexFormat::Uint32,
331 primitive_topology: PrimitiveTopology::LineList,
332 };
333
334 let point_mesh = GpuMesh {
335 vertex_buffer: BufferId(2),
336 index_buffer: BufferId(3),
337 index_count: 50,
338 index_format: IndexFormat::Uint32,
339 primitive_topology: PrimitiveTopology::PointList,
340 };
341
342 let mut gpu_meshes = Assets::<GpuMesh>::new();
343 gpu_meshes.insert(line_uuid, AssetHandle::new(line_mesh));
344 gpu_meshes.insert(point_uuid, AssetHandle::new(point_mesh));
345
346 let mut render_world = RenderWorld::default();
347 render_world.meshes.push(ExtractedMesh {
348 transform: Default::default(),
349 gpu_mesh_uuid: line_uuid,
350 material_uuid: None,
351 });
352 render_world.meshes.push(ExtractedMesh {
353 transform: Default::default(),
354 gpu_mesh_uuid: point_uuid,
355 material_uuid: None,
356 });
357
358 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
359 let cost = lane.estimate_cost(&render_world, &gpu_meshes_lock);
360
361 assert_eq!(
363 cost, 0.2,
364 "Cost should be 0.2 for 2 draw calls with no triangles"
365 );
366 }
367
368 #[test]
369 fn test_cost_estimation_multiple_meshes() {
370 use crate::render_lane::world::ExtractedMesh;
371 use khora_core::asset::AssetUUID;
372
373 let lane = SimpleUnlitLane::new();
374
375 let mesh1_uuid = AssetUUID::new();
377 let mesh2_uuid = AssetUUID::new();
378 let mesh3_uuid = AssetUUID::new();
379
380 let mesh1 = GpuMesh {
381 vertex_buffer: BufferId(0),
382 index_buffer: BufferId(1),
383 index_count: 600, index_format: IndexFormat::Uint32,
385 primitive_topology: PrimitiveTopology::TriangleList,
386 };
387
388 let mesh2 = GpuMesh {
389 vertex_buffer: BufferId(2),
390 index_buffer: BufferId(3),
391 index_count: 102, index_format: IndexFormat::Uint16,
393 primitive_topology: PrimitiveTopology::TriangleStrip,
394 };
395
396 let mesh3 = GpuMesh {
397 vertex_buffer: BufferId(4),
398 index_buffer: BufferId(5),
399 index_count: 150, index_format: IndexFormat::Uint32,
401 primitive_topology: PrimitiveTopology::TriangleList,
402 };
403
404 let mut gpu_meshes = Assets::<GpuMesh>::new();
405 gpu_meshes.insert(mesh1_uuid, AssetHandle::new(mesh1));
406 gpu_meshes.insert(mesh2_uuid, AssetHandle::new(mesh2));
407 gpu_meshes.insert(mesh3_uuid, AssetHandle::new(mesh3));
408
409 let mut render_world = RenderWorld::default();
410 render_world.meshes.push(ExtractedMesh {
411 transform: Default::default(),
412 gpu_mesh_uuid: mesh1_uuid,
413 material_uuid: None,
414 });
415 render_world.meshes.push(ExtractedMesh {
416 transform: Default::default(),
417 gpu_mesh_uuid: mesh2_uuid,
418 material_uuid: None,
419 });
420 render_world.meshes.push(ExtractedMesh {
421 transform: Default::default(),
422 gpu_mesh_uuid: mesh3_uuid,
423 material_uuid: None,
424 });
425
426 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
427 let cost = lane.estimate_cost(&render_world, &gpu_meshes_lock);
428
429 assert!(
432 (cost - 0.65).abs() < 0.0001,
433 "Cost should be approximately 0.65 for 350 triangles + 3 draw calls, got {}",
434 cost
435 );
436 }
437
438 #[test]
439 fn test_cost_estimation_missing_mesh() {
440 use crate::render_lane::world::ExtractedMesh;
441 use khora_core::asset::AssetUUID;
442
443 let lane = SimpleUnlitLane::new();
444 let gpu_meshes = Arc::new(RwLock::new(Assets::<GpuMesh>::new()));
445
446 let mut render_world = RenderWorld::default();
448 render_world.meshes.push(ExtractedMesh {
449 transform: Default::default(),
450 gpu_mesh_uuid: AssetUUID::new(),
451 material_uuid: None,
452 });
453
454 let cost = lane.estimate_cost(&render_world, &gpu_meshes);
455
456 assert_eq!(cost, 0.0, "Missing mesh should contribute zero cost");
458 }
459
460 #[test]
461 fn test_cost_estimation_degenerate_triangle_strip() {
462 use crate::render_lane::world::ExtractedMesh;
463 use khora_core::asset::AssetUUID;
464
465 let lane = SimpleUnlitLane::new();
466
467 let mesh_uuid = AssetUUID::new();
469 let gpu_mesh = GpuMesh {
470 vertex_buffer: BufferId(0),
471 index_buffer: BufferId(1),
472 index_count: 2,
473 index_format: IndexFormat::Uint16,
474 primitive_topology: PrimitiveTopology::TriangleStrip,
475 };
476
477 let mut gpu_meshes = Assets::<GpuMesh>::new();
478 gpu_meshes.insert(mesh_uuid, AssetHandle::new(gpu_mesh));
479
480 let mut render_world = RenderWorld::default();
481 render_world.meshes.push(ExtractedMesh {
482 transform: Default::default(),
483 gpu_mesh_uuid: mesh_uuid,
484 material_uuid: None,
485 });
486
487 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
488 let cost = lane.estimate_cost(&render_world, &gpu_meshes_lock);
489
490 assert_eq!(
492 cost, 0.1,
493 "Degenerate triangle strip should only cost draw call overhead"
494 );
495 }
496
497 #[test]
498 fn test_get_pipeline_for_material_with_none() {
499 let lane = SimpleUnlitLane::new();
500 let materials = Assets::<Box<dyn Material>>::new();
501
502 let pipeline = lane.get_pipeline_for_material(None, &materials);
503 assert_eq!(
504 pipeline,
505 RenderPipelineId(0),
506 "None material should use default pipeline"
507 );
508 }
509
510 #[test]
511 fn test_get_pipeline_for_material_not_found() {
512 use khora_core::asset::AssetUUID;
513
514 let lane = SimpleUnlitLane::new();
515 let materials = Assets::<Box<dyn Material>>::new();
516
517 let pipeline = lane.get_pipeline_for_material(Some(AssetUUID::new()), &materials);
518 assert_eq!(
519 pipeline,
520 RenderPipelineId(0),
521 "Missing material should use default pipeline"
522 );
523 }
524}