1use khora_core::{
30 asset::Material,
31 renderer::{
32 api::{
33 command::{
34 LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment,
35 RenderPassDescriptor, StoreOp,
36 },
37 core::RenderContext,
38 pipeline::enums::PrimitiveTopology,
39 pipeline::{LayoutKey, LayoutSpec, PipelineSpec, RenderPipelineId, ShaderVariantKey},
40 scene::GpuMesh,
41 },
42 traits::CommandEncoder,
43 },
44};
45use khora_data::assets::Assets;
46use khora_data::render::RenderWorld;
47use std::sync::{OnceLock, RwLock};
48
49pub struct SimpleUnlitLane {
61 pipeline: OnceLock<RenderPipelineId>,
65 camera_layout: OnceLock<khora_core::renderer::api::command::BindGroupLayoutId>,
66 model_layout: OnceLock<khora_core::renderer::api::command::BindGroupLayoutId>,
67 material_layout: OnceLock<khora_core::renderer::api::command::BindGroupLayoutId>,
68
69 camera_ring: std::sync::Mutex<
72 Option<khora_core::renderer::api::util::uniform_ring_buffer::UniformRingBuffer>,
73 >,
74 model_ring: std::sync::Mutex<
75 Option<khora_core::renderer::api::util::dynamic_uniform_buffer::DynamicUniformRingBuffer>,
76 >,
77 material_ring: std::sync::Mutex<
78 Option<khora_core::renderer::api::util::dynamic_uniform_buffer::DynamicUniformRingBuffer>,
79 >,
80}
81
82impl Default for SimpleUnlitLane {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88impl SimpleUnlitLane {
89 pub fn new() -> Self {
91 Self {
92 pipeline: OnceLock::new(),
93 camera_layout: OnceLock::new(),
94 model_layout: OnceLock::new(),
95 material_layout: OnceLock::new(),
96 camera_ring: std::sync::Mutex::new(None),
97 model_ring: std::sync::Mutex::new(None),
98 material_ring: std::sync::Mutex::new(None),
99 }
100 }
101}
102
103impl khora_core::lane::Lane for SimpleUnlitLane {
104 fn strategy_name(&self) -> &'static str {
105 "SimpleUnlit"
106 }
107
108 fn lane_kind(&self) -> khora_core::lane::LaneKind {
109 khora_core::lane::LaneKind::Render
110 }
111
112 fn estimate_cost(&self, ctx: &khora_core::lane::LaneContext) -> f32 {
113 let render_world = match ctx.get::<khora_core::lane::Ref<khora_data::render::RenderWorld>>()
114 {
115 Some(slot) => slot.get(),
116 None => return 1.0,
117 };
118 let gpu_meshes = match ctx.get::<std::sync::Arc<
119 std::sync::RwLock<
120 khora_data::assets::Assets<khora_core::renderer::api::scene::GpuMesh>,
121 >,
122 >>() {
123 Some(arc) => arc,
124 None => return 1.0,
125 };
126 self.estimate_render_cost(render_world, gpu_meshes)
127 }
128
129 fn on_initialize(
130 &self,
131 ctx: &mut khora_core::lane::LaneContext,
132 ) -> Result<(), khora_core::lane::LaneError> {
133 let device = ctx
134 .get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
135 .ok_or(khora_core::lane::LaneError::missing(
136 "Arc<dyn GraphicsDevice>",
137 ))?
138 .clone();
139 let pipeline_system = ctx
140 .get::<std::sync::Arc<dyn khora_core::renderer::traits::PipelineSystem>>()
141 .ok_or(khora_core::lane::LaneError::missing(
142 "Arc<dyn PipelineSystem>",
143 ))?
144 .clone();
145 self.on_gpu_init(device.as_ref(), pipeline_system.as_ref())
146 .map_err(|e| khora_core::lane::LaneError::InitializationFailed(Box::new(e)))
147 }
148
149 fn execute(
150 &self,
151 ctx: &mut khora_core::lane::LaneContext,
152 ) -> Result<(), khora_core::lane::LaneError> {
153 use khora_core::lane::{LaneError, Ref, Slot};
154 let device = ctx
155 .get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
156 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
157 .clone();
158 let gpu_meshes = ctx
159 .get::<std::sync::Arc<
160 std::sync::RwLock<
161 khora_data::assets::Assets<khora_core::renderer::api::scene::GpuMesh>,
162 >,
163 >>()
164 .ok_or(LaneError::missing("Arc<RwLock<Assets<GpuMesh>>>"))?
165 .clone();
166 let encoder = ctx
167 .get::<Slot<dyn khora_core::renderer::traits::CommandEncoder>>()
168 .ok_or(LaneError::missing("Slot<dyn CommandEncoder>"))?
169 .get();
170 let render_world = ctx
171 .get::<Ref<khora_data::render::RenderWorld>>()
172 .ok_or(LaneError::missing("Ref<RenderWorld>"))?
173 .get();
174 let color_target = ctx
175 .get::<khora_core::lane::ColorTarget>()
176 .ok_or(LaneError::missing("ColorTarget"))?
177 .0;
178 let depth_target = ctx
179 .get::<khora_core::lane::DepthTarget>()
180 .ok_or(LaneError::missing("DepthTarget"))?
181 .0;
182 let clear_color = ctx
183 .get::<khora_core::lane::ClearColor>()
184 .ok_or(LaneError::missing("ClearColor"))?
185 .0;
186 let shadow_atlas = ctx.get::<khora_core::lane::ShadowAtlasView>().map(|v| v.0);
187 let shadow_sampler = ctx
188 .get::<khora_core::lane::ShadowComparisonSampler>()
189 .map(|v| v.0);
190
191 let mut render_ctx = khora_core::renderer::api::core::RenderContext::new(
192 &color_target,
193 Some(&depth_target),
194 clear_color,
195 );
196 render_ctx.shadow_atlas = shadow_atlas.as_ref();
197 render_ctx.shadow_sampler = shadow_sampler.as_ref();
198
199 self.render(
200 render_world,
201 device.as_ref(),
202 encoder,
203 &render_ctx,
204 &gpu_meshes,
205 );
206 Ok(())
207 }
208
209 fn on_shutdown(&self, ctx: &mut khora_core::lane::LaneContext) {
210 if let Some(device) = ctx.get::<std::sync::Arc<dyn khora_core::renderer::GraphicsDevice>>()
211 {
212 self.on_gpu_shutdown(device.as_ref());
213 }
214 }
215
216 fn as_any(&self) -> &dyn std::any::Any {
217 self
218 }
219
220 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
221 self
222 }
223}
224
225impl SimpleUnlitLane {
226 pub fn get_pipeline_for_material(
228 &self,
229 _material: Option<&khora_core::asset::AssetHandle<Box<dyn Material>>>,
230 ) -> RenderPipelineId {
231 self.pipeline.get().copied().unwrap_or(RenderPipelineId(0))
234 }
235
236 fn render(
237 &self,
238 render_world: &RenderWorld,
239 device: &dyn khora_core::renderer::GraphicsDevice,
240 encoder: &mut dyn CommandEncoder,
241 render_ctx: &RenderContext,
242 gpu_meshes: &RwLock<Assets<GpuMesh>>,
243 ) {
244 use khora_core::renderer::api::{resource::CameraUniformData, scene::ModelUniforms};
245
246 let Some(view) = render_world.views.first() else {
251 let color_attachment = RenderPassColorAttachment {
252 view: render_ctx.color_target,
253 resolve_target: None,
254 ops: Operations {
255 load: LoadOp::Clear(render_ctx.clear_color),
256 store: StoreOp::Store,
257 },
258 base_array_layer: 0,
259 base_mip_level: 0,
260 };
261 let clear_desc = RenderPassDescriptor {
262 label: Some("Simple Unlit Clear-Only Pass"),
263 color_attachments: &[color_attachment],
264 depth_stencil_attachment: render_ctx.depth_target.map(|depth_view| {
265 RenderPassDepthStencilAttachment {
266 view: depth_view,
267 depth_ops: Some(Operations {
268 load: LoadOp::Clear(1.0),
269 store: StoreOp::Store,
270 }),
271 stencil_ops: None,
272 base_array_layer: 0,
273 }
274 }),
275 };
276 let _ = encoder.begin_render_pass(&clear_desc);
279 return;
280 };
281
282 let camera_uniforms = CameraUniformData {
284 view_projection: view.view_proj.to_cols_array_2d(),
285 camera_position: [view.position.x, view.position.y, view.position.z, 1.0],
286 };
287
288 let camera_bind_group = {
289 let mut lock = crate::lock_or_log!(
290 self.camera_ring.lock(),
291 "SimpleUnlitLane::render camera_ring"
292 );
293 let ring = match lock.as_mut() {
294 Some(r) => r,
295 None => {
296 log::warn!("SimpleUnlitLane: camera ring buffer not initialized");
297 return;
298 }
299 };
300 ring.advance();
301 if let Err(e) = ring.write(device, bytemuck::bytes_of(&camera_uniforms)) {
302 log::error!("Failed to write camera ring buffer: {:?}", e);
303 return;
304 }
305 *ring.current_bind_group()
306 };
307
308 let mut model_ring_lock =
310 crate::lock_or_log!(self.model_ring.lock(), "SimpleUnlitLane::render model_ring");
311 let model_ring = match model_ring_lock.as_mut() {
312 Some(mr) => {
313 mr.advance();
314 mr
315 }
316 None => return,
317 };
318
319 let mut material_ring_lock = crate::lock_or_log!(
320 self.material_ring.lock(),
321 "SimpleUnlitLane::render material_ring"
322 );
323 let material_ring = match material_ring_lock.as_mut() {
324 Some(mr) => {
325 mr.advance();
326 mr
327 }
328 None => return,
329 };
330
331 let gpu_mesh_assets =
333 crate::lock_or_log!(gpu_meshes.read(), "SimpleUnlitLane::render gpu_meshes");
334
335 let mut draw_commands = Vec::with_capacity(render_world.meshes.len());
337
338 for extracted_mesh in &render_world.meshes {
339 if let Some(gpu_mesh_handle) = gpu_mesh_assets.get(&extracted_mesh.cpu_mesh_uuid) {
340 let pipeline = self.get_pipeline_for_material(extracted_mesh.material.as_ref());
342
343 let model_mat = extracted_mesh.transform.to_matrix();
345
346 let normal_mat = if let Some(inverse) = model_mat.inverse() {
347 inverse.transpose()
348 } else {
349 continue; };
351
352 let mut base_color = khora_core::math::LinearRgba::WHITE;
353 if let Some(mat_handle) = &extracted_mesh.material {
354 base_color = mat_handle.base_color();
355 }
356
357 let model_uniforms = ModelUniforms {
358 model_matrix: model_mat.to_cols_array_2d(),
359 normal_matrix: normal_mat.to_cols_array_2d(),
360 };
361
362 let offset = match model_ring.push(device, bytemuck::bytes_of(&model_uniforms)) {
363 Ok(off) => off,
364 Err(_) => continue,
365 };
366 let model_bg = *model_ring.current_bind_group();
367
368 let material_uniforms = khora_core::renderer::api::scene::MaterialUniforms {
370 base_color,
371 emissive: khora_core::math::LinearRgba::BLACK,
372 ambient: khora_core::math::LinearRgba::BLACK,
373 pbr_factors: [0.0, 1.0, 0.5, 0.0],
374 };
375
376 let mat_offset =
377 match material_ring.push(device, bytemuck::bytes_of(&material_uniforms)) {
378 Ok(off) => off,
379 Err(_) => continue,
380 };
381 let material_bg = *material_ring.current_bind_group();
382
383 draw_commands.push(khora_core::renderer::api::command::DrawCommand {
384 pipeline,
385 vertex_buffer: gpu_mesh_handle.vertex_buffer,
386 index_buffer: gpu_mesh_handle.index_buffer,
387 index_format: gpu_mesh_handle.index_format,
388 index_count: gpu_mesh_handle.index_count,
389 model_bind_group: Some(model_bg),
390 model_offset: offset,
391 material_bind_group: Some(material_bg),
392 material_offset: mat_offset,
393 });
394 }
395 }
396
397 let color_attachment = RenderPassColorAttachment {
399 view: render_ctx.color_target,
400 resolve_target: None,
401 ops: Operations {
402 load: LoadOp::Clear(render_ctx.clear_color),
403 store: StoreOp::Store,
404 },
405 base_array_layer: 0,
406 base_mip_level: 0,
407 };
408
409 let render_pass_desc = RenderPassDescriptor {
410 label: Some("Simple Unlit Pass"),
411 color_attachments: &[color_attachment],
412 depth_stencil_attachment: render_ctx.depth_target.map(|depth_view| {
413 RenderPassDepthStencilAttachment {
414 view: depth_view,
415 depth_ops: Some(Operations {
416 load: LoadOp::Clear(1.0),
417 store: StoreOp::Store,
418 }),
419 stencil_ops: None,
420 base_array_layer: 0,
421 }
422 }),
423 };
424
425 let mut render_pass = encoder.begin_render_pass(&render_pass_desc);
427
428 render_pass.set_bind_group(0, &camera_bind_group, &[]);
430
431 let mut current_pipeline: Option<RenderPipelineId> = None;
433
434 for cmd in &draw_commands {
435 if current_pipeline != Some(cmd.pipeline) {
436 render_pass.set_pipeline(&cmd.pipeline);
437 current_pipeline = Some(cmd.pipeline);
438 }
439
440 if let Some(ref bg) = cmd.model_bind_group {
441 render_pass.set_bind_group(1, bg, &[cmd.model_offset]);
442 }
443
444 if let Some(ref bg) = cmd.material_bind_group {
445 render_pass.set_bind_group(2, bg, &[cmd.material_offset]);
446 }
447
448 render_pass.set_vertex_buffer(0, &cmd.vertex_buffer, 0);
449 render_pass.set_index_buffer(&cmd.index_buffer, 0, cmd.index_format);
450 render_pass.draw_indexed(0..cmd.index_count, 0, 0..1);
451 }
452 }
453
454 fn estimate_render_cost(
455 &self,
456 render_world: &RenderWorld,
457 gpu_meshes: &RwLock<Assets<GpuMesh>>,
458 ) -> f32 {
459 let gpu_mesh_assets = crate::lock_or_log!(
460 gpu_meshes.read(),
461 "SimpleUnlitLane::estimate_render_cost",
462 0.0
463 );
464
465 let mut total_triangles = 0u32;
466 let mut draw_call_count = 0u32;
467
468 for extracted_mesh in &render_world.meshes {
469 if let Some(gpu_mesh) = gpu_mesh_assets.get(&extracted_mesh.cpu_mesh_uuid) {
470 let triangle_count = match gpu_mesh.primitive_topology {
472 PrimitiveTopology::TriangleList => gpu_mesh.index_count / 3,
473 PrimitiveTopology::TriangleStrip => {
474 if gpu_mesh.index_count >= 3 {
475 gpu_mesh.index_count - 2
476 } else {
477 0
478 }
479 }
480 PrimitiveTopology::LineList
482 | PrimitiveTopology::LineStrip
483 | PrimitiveTopology::PointList => 0,
484 };
485
486 total_triangles += triangle_count;
487 draw_call_count += 1;
488 }
489 }
490
491 const TRIANGLE_COST: f32 = 0.001;
494 const DRAW_CALL_COST: f32 = 0.1;
495
496 (total_triangles as f32 * TRIANGLE_COST) + (draw_call_count as f32 * DRAW_CALL_COST)
497 }
498
499 fn on_gpu_init(
500 &self,
501 device: &dyn khora_core::renderer::GraphicsDevice,
502 pipeline_system: &dyn khora_core::renderer::traits::PipelineSystem,
503 ) -> Result<(), khora_core::renderer::error::RenderError> {
504 use khora_core::renderer::api::{
505 resource::CameraUniformData, scene::ModelUniforms,
506 util::uniform_ring_buffer::UniformRingBuffer,
507 };
508
509 log::info!("SimpleUnlitLane: Initializing GPU resources...");
510
511 let variant = ShaderVariantKey::empty();
516 let camera_layout = pipeline_system.layout(device, LayoutKey::Camera, &variant)?;
517 let model_layout = pipeline_system.inline_layout(
518 device,
519 UNLIT_MODEL_LAYOUT_LABEL,
520 &unlit_model_layout_entries(),
521 )?;
522 let material_layout = pipeline_system.inline_layout(
523 device,
524 UNLIT_MATERIAL_LAYOUT_LABEL,
525 &unlit_material_layout_entries(),
526 )?;
527
528 let pipeline_id = pipeline_system.pipeline(device, &pipeline_spec(device))?;
530
531 let _ = self.camera_layout.set(camera_layout);
535 let _ = self.model_layout.set(model_layout);
536 let _ = self.material_layout.set(material_layout);
537 let _ = self.pipeline.set(pipeline_id);
538
539 let camera_ring = UniformRingBuffer::new(
540 device,
541 camera_layout,
542 0,
543 std::mem::size_of::<CameraUniformData>() as u64,
544 "Camera Uniform Ring Runlit",
545 )
546 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
547
548 *crate::render_lane::util::lock::mutex_lock_render(
549 &self.camera_ring,
550 "SimpleUnlit init.camera_ring",
551 )? = Some(camera_ring);
552
553 let model_ring =
554 khora_core::renderer::api::util::dynamic_uniform_buffer::DynamicUniformRingBuffer::new(
555 device,
556 model_layout,
557 0,
558 std::mem::size_of::<ModelUniforms>() as u32,
559 khora_core::renderer::api::util::dynamic_uniform_buffer::DEFAULT_MAX_ELEMENTS,
560 khora_core::renderer::api::util::dynamic_uniform_buffer::MIN_UNIFORM_ALIGNMENT,
561 "Model Dynamic Ring Runlit",
562 )
563 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
564
565 *crate::render_lane::util::lock::mutex_lock_render(
566 &self.model_ring,
567 "SimpleUnlit init.model_ring",
568 )? = Some(model_ring);
569
570 let material_ring =
571 khora_core::renderer::api::util::dynamic_uniform_buffer::DynamicUniformRingBuffer::new(
572 device,
573 material_layout,
574 0, std::mem::size_of::<khora_core::renderer::api::scene::MaterialUniforms>() as u32,
576 khora_core::renderer::api::util::dynamic_uniform_buffer::DEFAULT_MAX_ELEMENTS,
577 khora_core::renderer::api::util::dynamic_uniform_buffer::MIN_UNIFORM_ALIGNMENT,
578 "Material Dynamic Ring Runlit",
579 )
580 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
581
582 *crate::render_lane::util::lock::mutex_lock_render(
583 &self.material_ring,
584 "SimpleUnlit init.material_ring",
585 )? = Some(material_ring);
586
587 Ok(())
588 }
589
590 fn on_gpu_shutdown(&self, device: &dyn khora_core::renderer::GraphicsDevice) {
591 if let Some(ring) = self.camera_ring.lock().ok().and_then(|mut g| g.take()) {
595 ring.destroy(device);
596 }
597 if let Some(ring) = self.model_ring.lock().ok().and_then(|mut g| g.take()) {
598 ring.destroy(device);
599 }
600 if let Some(ring) = self.material_ring.lock().ok().and_then(|mut g| g.take()) {
601 ring.destroy(device);
602 }
603 }
604}
605
606const UNLIT_MODEL_LAYOUT_LABEL: &str = "simple_unlit_model_layout";
610const UNLIT_MATERIAL_LAYOUT_LABEL: &str = "simple_unlit_material_layout";
612
613fn unlit_model_layout_entries() -> Vec<khora_core::renderer::api::command::BindGroupLayoutEntry> {
615 use khora_core::renderer::api::command::{
616 BindGroupLayoutEntry, BindingType, BufferBindingType,
617 };
618 use khora_core::renderer::api::scene::ModelUniforms;
619 use khora_core::renderer::api::util::ShaderStageFlags;
620 vec![BindGroupLayoutEntry {
621 binding: 0,
622 visibility: ShaderStageFlags::VERTEX,
623 ty: BindingType::Buffer {
624 ty: BufferBindingType::Uniform,
625 has_dynamic_offset: true,
626 min_binding_size: std::num::NonZeroU64::new(std::mem::size_of::<ModelUniforms>() as u64),
627 },
628 }]
629}
630
631fn unlit_material_layout_entries() -> Vec<khora_core::renderer::api::command::BindGroupLayoutEntry>
635{
636 use khora_core::renderer::api::command::{
637 BindGroupLayoutEntry, BindingType, BufferBindingType,
638 };
639 use khora_core::renderer::api::scene::MaterialUniforms;
640 use khora_core::renderer::api::util::ShaderStageFlags;
641 vec![BindGroupLayoutEntry {
642 binding: 0,
643 visibility: ShaderStageFlags::FRAGMENT,
644 ty: BindingType::Buffer {
645 ty: BufferBindingType::Uniform,
646 has_dynamic_offset: true,
647 min_binding_size: std::num::NonZeroU64::new(
648 std::mem::size_of::<MaterialUniforms>() as u64
649 ),
650 },
651 }]
652}
653
654fn pipeline_spec(device: &dyn khora_core::renderer::GraphicsDevice) -> PipelineSpec {
658 use khora_core::renderer::api::pipeline::enums::{
659 CompareFunction, VertexFormat, VertexStepMode,
660 };
661 use khora_core::renderer::api::pipeline::state::{
662 ColorWrites, DepthBiasState, StencilFaceState,
663 };
664 use khora_core::renderer::api::pipeline::{
665 ColorTargetStateDescriptor, DepthStencilStateDescriptor, MultisampleStateDescriptor,
666 PrimitiveStateDescriptor, VertexAttributeDescriptor, VertexBufferLayoutDescriptor,
667 };
668 use khora_core::renderer::api::util::{SampleCount, TextureFormat};
669 use std::borrow::Cow;
670
671 PipelineSpec {
672 label: "SimpleUnlit Pipeline",
673 shader: "khora::pipelines::unlit",
674 variant: ShaderVariantKey::empty(),
675 bind_group_layouts: vec![
676 LayoutSpec::Named(LayoutKey::Camera),
677 LayoutSpec::Inline {
678 label: UNLIT_MODEL_LAYOUT_LABEL,
679 entries: Cow::Owned(unlit_model_layout_entries()),
680 },
681 LayoutSpec::Inline {
682 label: UNLIT_MATERIAL_LAYOUT_LABEL,
683 entries: Cow::Owned(unlit_material_layout_entries()),
684 },
685 ],
686 vertex_buffers: vec![VertexBufferLayoutDescriptor {
687 array_stride: 32,
688 step_mode: VertexStepMode::Vertex,
689 attributes: Cow::Owned(vec![
690 VertexAttributeDescriptor {
691 format: VertexFormat::Float32x3,
692 offset: 0,
693 shader_location: 0,
694 },
695 VertexAttributeDescriptor {
696 format: VertexFormat::Float32x3,
697 offset: 12,
698 shader_location: 1,
699 },
700 VertexAttributeDescriptor {
701 format: VertexFormat::Float32x2,
702 offset: 24,
703 shader_location: 2,
704 },
705 ]),
706 }],
707 vs_entry: "vs_main",
708 fs_entry: Some("fs_main"),
709 primitive: PrimitiveStateDescriptor {
710 topology: PrimitiveTopology::TriangleList,
711 ..Default::default()
712 },
713 depth_stencil: Some(DepthStencilStateDescriptor {
714 format: TextureFormat::Depth32Float,
715 depth_write_enabled: true,
716 depth_compare: CompareFunction::Less,
717 stencil_front: StencilFaceState::default(),
718 stencil_back: StencilFaceState::default(),
719 stencil_read_mask: 0,
720 stencil_write_mask: 0,
721 bias: DepthBiasState::default(),
722 }),
723 color_targets: vec![ColorTargetStateDescriptor {
724 format: device
725 .get_surface_format()
726 .unwrap_or(TextureFormat::Rgba8UnormSrgb),
727 blend: None,
728 write_mask: ColorWrites::ALL,
729 }],
730 multisample: MultisampleStateDescriptor {
731 count: SampleCount::X1,
732 mask: !0,
733 alpha_to_coverage_enabled: false,
734 },
735 }
736}
737
738#[cfg(test)]
739mod tests {
740 use super::*;
741 use khora_core::lane::Lane;
742 use khora_core::{
743 asset::AssetHandle,
744 renderer::api::{
745 pipeline::enums::PrimitiveTopology, resource::BufferId, util::IndexFormat,
746 },
747 };
748 use khora_data::render::ExtractedMesh;
749 use std::sync::Arc;
750
751 #[test]
752 fn test_simple_unlit_lane_creation() {
753 let lane = SimpleUnlitLane::new();
754 assert_eq!(lane.strategy_name(), "SimpleUnlit");
755 }
756
757 #[test]
758 fn test_default_construction() {
759 let lane = SimpleUnlitLane::new();
760 assert_eq!(lane.strategy_name(), "SimpleUnlit");
761 }
762
763 #[test]
764 fn test_cost_estimation_empty_world() {
765 let lane = SimpleUnlitLane::new();
766 let render_world = RenderWorld::default();
767 let gpu_meshes = Arc::new(RwLock::new(Assets::<GpuMesh>::new()));
768
769 let cost = lane.estimate_render_cost(&render_world, &gpu_meshes);
770 assert_eq!(cost, 0.0, "Empty world should have zero cost");
771 }
772
773 #[test]
774 fn test_cost_estimation_triangle_list() {
775 use khora_core::asset::AssetUUID;
776
777 let lane = SimpleUnlitLane::new();
778
779 let mesh_uuid = AssetUUID::new();
781 let gpu_mesh = GpuMesh {
782 vertex_buffer: BufferId(0),
783 index_buffer: BufferId(1),
784 index_count: 300,
785 index_format: IndexFormat::Uint32,
786 primitive_topology: PrimitiveTopology::TriangleList,
787 };
788 let gpu_mesh_handle = AssetHandle::new(gpu_mesh);
789 let mut gpu_meshes = Assets::<GpuMesh>::new();
790 gpu_meshes.insert(mesh_uuid, gpu_mesh_handle.clone());
791
792 let mut render_world = RenderWorld::default();
793 render_world.meshes.push(ExtractedMesh {
794 transform: Default::default(),
795 cpu_mesh_uuid: mesh_uuid,
796 gpu_mesh: gpu_mesh_handle,
797 material: None,
798 gpu_material: None,
799 });
800
801 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
802 let cost = lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
803
804 assert_eq!(
806 cost, 0.2,
807 "Cost should be 0.2 for 100 triangles + 1 draw call"
808 );
809 }
810
811 #[test]
812 fn test_cost_estimation_triangle_strip() {
813 use khora_core::asset::AssetUUID;
814
815 let lane = SimpleUnlitLane::new();
816
817 let mesh_uuid = AssetUUID::new();
819 let gpu_mesh = GpuMesh {
820 vertex_buffer: BufferId(0),
821 index_buffer: BufferId(1),
822 index_count: 52,
823 index_format: IndexFormat::Uint16,
824 primitive_topology: PrimitiveTopology::TriangleStrip,
825 };
826 let gpu_mesh_handle = AssetHandle::new(gpu_mesh);
827 let mut gpu_meshes = Assets::<GpuMesh>::new();
828 gpu_meshes.insert(mesh_uuid, gpu_mesh_handle.clone());
829
830 let mut render_world = RenderWorld::default();
831 render_world.meshes.push(ExtractedMesh {
832 transform: Default::default(),
833 cpu_mesh_uuid: mesh_uuid,
834 gpu_mesh: gpu_mesh_handle,
835 material: None,
836 gpu_material: None,
837 });
838
839 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
840 let cost = lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
841
842 assert_eq!(
844 cost, 0.15,
845 "Cost should be 0.15 for 50 triangles + 1 draw call"
846 );
847 }
848
849 #[test]
850 fn test_cost_estimation_lines_and_points() {
851 use khora_core::asset::AssetUUID;
852
853 let lane = SimpleUnlitLane::new();
854
855 let line_uuid = AssetUUID::new();
857 let point_uuid = AssetUUID::new();
858
859 let line_mesh = GpuMesh {
860 vertex_buffer: BufferId(0),
861 index_buffer: BufferId(1),
862 index_count: 100,
863 index_format: IndexFormat::Uint32,
864 primitive_topology: PrimitiveTopology::LineList,
865 };
866
867 let point_mesh = GpuMesh {
868 vertex_buffer: BufferId(2),
869 index_buffer: BufferId(3),
870 index_count: 50,
871 index_format: IndexFormat::Uint32,
872 primitive_topology: PrimitiveTopology::PointList,
873 };
874 let line_mesh_handle = AssetHandle::new(line_mesh);
875 let point_mesh_handle = AssetHandle::new(point_mesh);
876
877 let mut gpu_meshes = Assets::<GpuMesh>::new();
878 gpu_meshes.insert(line_uuid, line_mesh_handle.clone());
879 gpu_meshes.insert(point_uuid, point_mesh_handle.clone());
880
881 let mut render_world = RenderWorld::default();
882 render_world.meshes.push(ExtractedMesh {
883 transform: Default::default(),
884 cpu_mesh_uuid: line_uuid,
885 gpu_mesh: line_mesh_handle,
886 material: None,
887 gpu_material: None,
888 });
889 render_world.meshes.push(ExtractedMesh {
890 transform: Default::default(),
891 cpu_mesh_uuid: point_uuid,
892 gpu_mesh: point_mesh_handle,
893 material: None,
894 gpu_material: None,
895 });
896
897 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
898 let cost = lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
899
900 assert_eq!(
902 cost, 0.2,
903 "Cost should be 0.2 for 2 draw calls with no triangles"
904 );
905 }
906
907 #[test]
908 fn test_cost_estimation_multiple_meshes() {
909 use khora_core::asset::AssetUUID;
910
911 let lane = SimpleUnlitLane::new();
912
913 let mesh1_uuid = AssetUUID::new();
915 let mesh2_uuid = AssetUUID::new();
916 let mesh3_uuid = AssetUUID::new();
917
918 let mesh1 = GpuMesh {
919 vertex_buffer: BufferId(0),
920 index_buffer: BufferId(1),
921 index_count: 600, index_format: IndexFormat::Uint32,
923 primitive_topology: PrimitiveTopology::TriangleList,
924 };
925
926 let mesh2 = GpuMesh {
927 vertex_buffer: BufferId(2),
928 index_buffer: BufferId(3),
929 index_count: 102, index_format: IndexFormat::Uint16,
931 primitive_topology: PrimitiveTopology::TriangleStrip,
932 };
933
934 let mesh3 = GpuMesh {
935 vertex_buffer: BufferId(4),
936 index_buffer: BufferId(5),
937 index_count: 150, index_format: IndexFormat::Uint32,
939 primitive_topology: PrimitiveTopology::TriangleList,
940 };
941
942 let mut gpu_meshes = Assets::<GpuMesh>::new();
943 gpu_meshes.insert(mesh1_uuid, AssetHandle::new(mesh1));
944 gpu_meshes.insert(mesh2_uuid, AssetHandle::new(mesh2));
945 gpu_meshes.insert(mesh3_uuid, AssetHandle::new(mesh3));
946
947 let mut render_world = RenderWorld::default();
948 render_world.meshes.push(ExtractedMesh {
949 transform: Default::default(),
950 cpu_mesh_uuid: mesh1_uuid,
951 gpu_mesh: AssetHandle::new(create_test_mesh(600)),
952 material: None,
953 gpu_material: None,
954 });
955 render_world.meshes.push(ExtractedMesh {
956 transform: Default::default(),
957 cpu_mesh_uuid: mesh2_uuid,
958 gpu_mesh: AssetHandle::new(create_test_mesh(102)),
959 material: None,
960 gpu_material: None,
961 });
962 render_world.meshes.push(ExtractedMesh {
963 transform: Default::default(),
964 cpu_mesh_uuid: mesh3_uuid,
965 gpu_mesh: AssetHandle::new(create_test_mesh(150)),
966 material: None,
967 gpu_material: None,
968 });
969
970 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
971 let cost = lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
972
973 assert!(
976 (cost - 0.65).abs() < 0.0001,
977 "Cost should be approximately 0.65 for 350 triangles + 3 draw calls, got {}",
978 cost
979 );
980 }
981
982 fn create_test_mesh(index_count: u32) -> GpuMesh {
984 GpuMesh {
985 vertex_buffer: BufferId(0),
986 index_buffer: BufferId(1),
987 index_count,
988 index_format: IndexFormat::Uint32,
989 primitive_topology: PrimitiveTopology::TriangleList,
990 }
991 }
992
993 #[test]
994 fn test_cost_estimation_missing_mesh() {
995 use khora_core::asset::AssetUUID;
996
997 let lane = SimpleUnlitLane::new();
998 let gpu_meshes = Arc::new(RwLock::new(Assets::<GpuMesh>::new()));
999
1000 let mut render_world = RenderWorld::default();
1002 render_world.meshes.push(ExtractedMesh {
1003 transform: Default::default(),
1004 cpu_mesh_uuid: AssetUUID::new(),
1005 gpu_mesh: AssetHandle::new(create_test_mesh(300)),
1006 material: None,
1007 gpu_material: None,
1008 });
1009
1010 let cost = lane.estimate_render_cost(&render_world, &gpu_meshes);
1011
1012 assert_eq!(cost, 0.0, "Missing mesh should contribute zero cost");
1014 }
1015
1016 #[test]
1017 fn test_cost_estimation_degenerate_triangle_strip() {
1018 use khora_core::asset::AssetUUID;
1019
1020 let lane = SimpleUnlitLane::new();
1021
1022 let mesh_uuid = AssetUUID::new();
1024 let gpu_mesh = GpuMesh {
1025 vertex_buffer: BufferId(0),
1026 index_buffer: BufferId(1),
1027 index_count: 2,
1028 index_format: IndexFormat::Uint16,
1029 primitive_topology: PrimitiveTopology::TriangleStrip,
1030 };
1031
1032 let handle = AssetHandle::new(gpu_mesh);
1033 let mut gpu_meshes = Assets::<GpuMesh>::new();
1034 gpu_meshes.insert(mesh_uuid, handle.clone());
1035
1036 let mut render_world = RenderWorld::default();
1037 render_world.meshes.push(ExtractedMesh {
1038 transform: Default::default(),
1039 cpu_mesh_uuid: mesh_uuid,
1040 gpu_mesh: handle,
1041 material: None,
1042 gpu_material: None,
1043 });
1044
1045 let gpu_meshes_lock = Arc::new(RwLock::new(gpu_meshes));
1046 let cost = lane.estimate_render_cost(&render_world, &gpu_meshes_lock);
1047
1048 assert_eq!(
1050 cost, 0.1,
1051 "Degenerate triangle strip should only cost draw call overhead"
1052 );
1053 }
1054
1055 #[test]
1056 fn test_get_pipeline_for_material_with_none() {
1057 let lane = SimpleUnlitLane::new();
1058
1059 let pipeline = lane.get_pipeline_for_material(None);
1060 assert_eq!(
1061 pipeline,
1062 RenderPipelineId(0),
1063 "None material should use default pipeline"
1064 );
1065 }
1066
1067 #[test]
1068 fn test_get_pipeline_for_material_not_found() {
1069 let lane = SimpleUnlitLane::new();
1070
1071 let pipeline = lane.get_pipeline_for_material(None);
1074 assert_eq!(
1075 pipeline,
1076 RenderPipelineId(0),
1077 "Missing material should use default pipeline"
1078 );
1079 }
1080}