1use khora_core::lane::{Lane, LaneContext, LaneError, LaneKind, Ref, Slot};
34use khora_core::renderer::api::command::{BindGroupId, BindGroupLayoutId};
35use khora_core::renderer::api::pipeline::RenderPipelineId;
36use khora_core::renderer::api::resource::BufferId;
37use khora_core::renderer::api::scene::GpuMesh;
38use khora_core::renderer::api::util::dynamic_uniform_buffer::DynamicUniformRingBuffer;
39use khora_core::renderer::traits::CommandEncoder;
40use khora_data::assets::Assets;
41use khora_data::render::{RenderWorld, WireframeConfig};
42use std::sync::{Arc, Mutex, OnceLock, RwLock};
43
44pub type SharedWireframeConfig = Arc<Mutex<WireframeConfig>>;
47
48#[repr(C, align(16))]
51#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
52struct WireframeMaterialUniforms {
53 color: [f32; 4],
54 line_width: f32,
55 _padding: [f32; 3],
56}
57
58#[derive(Debug, Default)]
60pub struct WireframeLane {
61 pipeline: OnceLock<RenderPipelineId>,
62 camera_layout: OnceLock<BindGroupLayoutId>,
63 material_layout: OnceLock<BindGroupLayoutId>,
64 camera_buffer: OnceLock<BufferId>,
65 camera_bind_group: OnceLock<BindGroupId>,
66 material_buffer: OnceLock<BufferId>,
67 material_bind_group: OnceLock<BindGroupId>,
68 model_ring: Mutex<Option<DynamicUniformRingBuffer>>,
71}
72
73fn init_gpu_resources(
76 lane: &WireframeLane,
77 device: &dyn khora_core::renderer::GraphicsDevice,
78 pipeline_system: &dyn khora_core::renderer::traits::PipelineSystem,
79) -> Result<(), khora_core::renderer::error::RenderError> {
80 use khora_core::renderer::api::command::{
81 BindGroupDescriptor, BindGroupEntry, BindingResource, BufferBinding,
82 };
83 use khora_core::renderer::api::pipeline::{LayoutKey, ShaderVariantKey};
84 use khora_core::renderer::api::resource::{BufferDescriptor, BufferUsage, CameraUniformData};
85 use khora_core::renderer::api::scene::ModelUniforms;
86 use khora_core::renderer::api::util::dynamic_uniform_buffer::{
87 DEFAULT_MAX_ELEMENTS, MIN_UNIFORM_ALIGNMENT,
88 };
89 use khora_core::renderer::api::util::ShaderStageFlags;
90 use std::borrow::Cow;
91
92 let camera_layout = pipeline_system.inline_layout(
96 device,
97 WIREFRAME_CAMERA_LAYOUT_LABEL,
98 &wireframe_uniform_layout_entries(ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT),
99 )?;
100 let material_layout = pipeline_system.inline_layout(
101 device,
102 WIREFRAME_MATERIAL_LAYOUT_LABEL,
103 &wireframe_uniform_layout_entries(ShaderStageFlags::FRAGMENT),
104 )?;
105 let model_layout =
106 pipeline_system.layout(device, LayoutKey::Model, &ShaderVariantKey::empty())?;
107
108 let pipeline_id = pipeline_system.pipeline(device, &wireframe_pipeline_spec(device))?;
109
110 let camera_buffer = device
111 .create_buffer(&BufferDescriptor {
112 label: Some(Cow::Borrowed("wireframe_camera_ubo")),
113 size: std::mem::size_of::<CameraUniformData>() as u64,
114 usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
115 mapped_at_creation: false,
116 })
117 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
118 let camera_bind_group = device
119 .create_bind_group(&BindGroupDescriptor {
120 label: Some("wireframe_camera_bg"),
121 layout: camera_layout,
122 entries: &[BindGroupEntry {
123 binding: 0,
124 resource: BindingResource::Buffer(BufferBinding {
125 buffer: camera_buffer,
126 offset: 0,
127 size: None,
128 }),
129 _phantom: std::marker::PhantomData,
130 }],
131 })
132 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
133
134 let material_buffer = device
135 .create_buffer(&BufferDescriptor {
136 label: Some(Cow::Borrowed("wireframe_material_ubo")),
137 size: std::mem::size_of::<WireframeMaterialUniforms>() as u64,
138 usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
139 mapped_at_creation: false,
140 })
141 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
142 let material_bind_group = device
143 .create_bind_group(&BindGroupDescriptor {
144 label: Some("wireframe_material_bg"),
145 layout: material_layout,
146 entries: &[BindGroupEntry {
147 binding: 0,
148 resource: BindingResource::Buffer(BufferBinding {
149 buffer: material_buffer,
150 offset: 0,
151 size: None,
152 }),
153 _phantom: std::marker::PhantomData,
154 }],
155 })
156 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
157
158 let model_ring = DynamicUniformRingBuffer::new(
159 device,
160 model_layout,
161 0,
162 std::mem::size_of::<ModelUniforms>() as u32,
163 DEFAULT_MAX_ELEMENTS,
164 MIN_UNIFORM_ALIGNMENT,
165 "Wireframe Model Ring",
166 )
167 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
168
169 let _ = lane.camera_layout.set(camera_layout);
170 let _ = lane.material_layout.set(material_layout);
171 let _ = lane.camera_buffer.set(camera_buffer);
172 let _ = lane.camera_bind_group.set(camera_bind_group);
173 let _ = lane.material_buffer.set(material_buffer);
174 let _ = lane.material_bind_group.set(material_bind_group);
175 *crate::render_lane::util::lock::mutex_lock_render(
176 &lane.model_ring,
177 "WireframeLane init.model_ring",
178 )? = Some(model_ring);
179 let _ = lane.pipeline.set(pipeline_id);
180 Ok(())
181}
182
183const WIREFRAME_CAMERA_LAYOUT_LABEL: &str = "wireframe_camera_layout";
185const WIREFRAME_MATERIAL_LAYOUT_LABEL: &str = "wireframe_material_layout";
186
187fn wireframe_uniform_layout_entries(
189 visibility: khora_core::renderer::api::util::ShaderStageFlags,
190) -> Vec<khora_core::renderer::api::command::BindGroupLayoutEntry> {
191 use khora_core::renderer::api::command::{
192 BindGroupLayoutEntry, BindingType, BufferBindingType,
193 };
194 vec![BindGroupLayoutEntry {
195 binding: 0,
196 visibility,
197 ty: BindingType::Buffer {
198 ty: BufferBindingType::Uniform,
199 has_dynamic_offset: false,
200 min_binding_size: None,
201 },
202 }]
203}
204
205fn wireframe_pipeline_spec(
208 device: &dyn khora_core::renderer::GraphicsDevice,
209) -> khora_core::renderer::api::pipeline::PipelineSpec {
210 use khora_core::renderer::api::pipeline::enums::{
211 CompareFunction, VertexFormat, VertexStepMode,
212 };
213 use khora_core::renderer::api::pipeline::state::{
214 BlendStateDescriptor, ColorWrites, DepthBiasState, StencilFaceState,
215 };
216 use khora_core::renderer::api::pipeline::{
217 ColorTargetStateDescriptor, DepthStencilStateDescriptor, LayoutKey, LayoutSpec,
218 MultisampleStateDescriptor, PipelineSpec, PrimitiveStateDescriptor, ShaderVariantKey,
219 VertexAttributeDescriptor, VertexBufferLayoutDescriptor,
220 };
221 use khora_core::renderer::api::util::ShaderStageFlags;
222 use khora_core::renderer::api::util::{SampleCount, TextureFormat};
223 use std::borrow::Cow;
224
225 PipelineSpec {
226 label: "Wireframe Pipeline",
227 shader: "khora::pipelines::wireframe",
228 variant: ShaderVariantKey::empty(),
229 bind_group_layouts: vec![
230 LayoutSpec::Inline {
231 label: WIREFRAME_CAMERA_LAYOUT_LABEL,
232 entries: Cow::Owned(wireframe_uniform_layout_entries(
233 ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
234 )),
235 },
236 LayoutSpec::Named(LayoutKey::Model),
237 LayoutSpec::Inline {
238 label: WIREFRAME_MATERIAL_LAYOUT_LABEL,
239 entries: Cow::Owned(wireframe_uniform_layout_entries(ShaderStageFlags::FRAGMENT)),
240 },
241 ],
242 vertex_buffers: vec![VertexBufferLayoutDescriptor {
243 array_stride: 32,
244 step_mode: VertexStepMode::Vertex,
245 attributes: Cow::Owned(vec![
246 VertexAttributeDescriptor {
247 format: VertexFormat::Float32x3,
248 offset: 0,
249 shader_location: 0,
250 },
251 VertexAttributeDescriptor {
252 format: VertexFormat::Float32x3,
253 offset: 12,
254 shader_location: 1,
255 },
256 ]),
257 }],
258 vs_entry: "vs_main",
259 fs_entry: Some("fs_main"),
260 primitive: PrimitiveStateDescriptor::default(),
261 depth_stencil: Some(DepthStencilStateDescriptor {
262 format: TextureFormat::Depth32Float,
263 depth_write_enabled: false,
266 depth_compare: CompareFunction::LessEqual,
267 stencil_front: StencilFaceState::default(),
268 stencil_back: StencilFaceState::default(),
269 stencil_read_mask: 0,
270 stencil_write_mask: 0,
271 bias: DepthBiasState::default(),
272 }),
273 color_targets: vec![ColorTargetStateDescriptor {
274 format: device
275 .get_surface_format()
276 .unwrap_or(TextureFormat::Rgba8UnormSrgb),
277 blend: Some(BlendStateDescriptor::alpha_blending()),
278 write_mask: ColorWrites::ALL,
279 }],
280 multisample: MultisampleStateDescriptor {
281 count: SampleCount::X1,
282 mask: !0,
283 alpha_to_coverage_enabled: false,
284 },
285 }
286}
287
288#[allow(clippy::too_many_arguments)]
289fn render_wireframe(
290 lane: &WireframeLane,
291 device: &dyn khora_core::renderer::GraphicsDevice,
292 encoder: &mut dyn CommandEncoder,
293 color_target: khora_core::renderer::api::resource::TextureViewId,
294 depth_target: khora_core::renderer::api::resource::TextureViewId,
295 view: &khora_data::render::ExtractedView,
296 render_world: &RenderWorld,
297 gpu_meshes: &RwLock<Assets<GpuMesh>>,
298 config: &WireframeConfig,
299) {
300 use khora_core::renderer::api::command::{
301 LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment,
302 RenderPassDescriptor, StoreOp,
303 };
304 use khora_core::renderer::api::resource::CameraUniformData;
305 use khora_core::renderer::api::scene::ModelUniforms;
306
307 let (
308 Some(pipeline),
309 Some(camera_buffer),
310 Some(camera_bg),
311 Some(material_buffer),
312 Some(material_bg),
313 ) = (
314 lane.pipeline.get().copied(),
315 lane.camera_buffer.get().copied(),
316 lane.camera_bind_group.get().copied(),
317 lane.material_buffer.get().copied(),
318 lane.material_bind_group.get().copied(),
319 )
320 else {
321 log::warn!("WireframeLane: GPU resources not initialized, skipping");
322 return;
323 };
324
325 let camera_uniforms = CameraUniformData {
328 view_projection: view.view_proj.to_cols_array_2d(),
329 camera_position: [view.position.x, view.position.y, view.position.z, 1.0],
330 };
331 if let Err(e) = device.write_buffer(camera_buffer, 0, bytemuck::bytes_of(&camera_uniforms)) {
332 log::error!("WireframeLane: camera buffer write failed: {:?}", e);
333 return;
334 }
335 let material_uniforms = WireframeMaterialUniforms {
336 color: [
337 config.line_color.r,
338 config.line_color.g,
339 config.line_color.b,
340 config.line_color.a,
341 ],
342 line_width: config.line_width,
343 _padding: [0.0; 3],
344 };
345 if let Err(e) = device.write_buffer(material_buffer, 0, bytemuck::bytes_of(&material_uniforms))
346 {
347 log::error!("WireframeLane: material buffer write failed: {:?}", e);
348 return;
349 }
350
351 let gpu_mesh_assets =
352 crate::lock_or_log!(gpu_meshes.read(), "WireframeLane::render gpu_meshes");
353 let mut model_ring_lock =
354 crate::lock_or_log!(lane.model_ring.lock(), "WireframeLane::render model_ring");
355 let model_ring = match model_ring_lock.as_mut() {
356 Some(r) => r,
357 None => {
358 log::warn!("WireframeLane: model ring not initialized");
359 return;
360 }
361 };
362 model_ring.advance();
363
364 use khora_core::renderer::api::util::IndexFormat;
367 let mut draws: Vec<(u32, BufferId, BufferId, IndexFormat, u32)> =
368 Vec::with_capacity(render_world.meshes.len());
369 for extracted_mesh in &render_world.meshes {
370 let Some(gpu_mesh) = gpu_mesh_assets.get(&extracted_mesh.cpu_mesh_uuid) else {
371 continue;
372 };
373 let model_mat = extracted_mesh.transform.to_matrix();
374 let normal_mat = model_mat
375 .inverse()
376 .map(|inv| inv.transpose())
377 .unwrap_or(model_mat);
378 let model_uniforms = ModelUniforms {
379 model_matrix: model_mat.to_cols_array_2d(),
380 normal_matrix: normal_mat.to_cols_array_2d(),
381 };
382 match model_ring.push(device, bytemuck::bytes_of(&model_uniforms)) {
383 Ok(offset) => draws.push((
384 offset,
385 gpu_mesh.vertex_buffer,
386 gpu_mesh.index_buffer,
387 gpu_mesh.index_format,
388 gpu_mesh.index_count,
389 )),
390 Err(e) => {
391 log::error!("WireframeLane: model push failed: {:?}", e);
392 return;
393 }
394 }
395 }
396 let model_bg = *model_ring.current_bind_group();
397
398 let color_attachment = RenderPassColorAttachment {
400 view: &color_target,
401 resolve_target: None,
402 ops: Operations {
403 load: LoadOp::Load,
404 store: StoreOp::Store,
405 },
406 base_array_layer: 0,
407 base_mip_level: 0,
408 };
409 let pass_desc = RenderPassDescriptor {
410 label: Some("Wireframe Overlay Pass"),
411 color_attachments: &[color_attachment],
412 depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
413 view: &depth_target,
414 depth_ops: Some(Operations {
415 load: LoadOp::Load,
416 store: StoreOp::Store,
417 }),
418 stencil_ops: None,
419 base_array_layer: 0,
420 }),
421 };
422
423 let mut pass = encoder.begin_render_pass(&pass_desc);
424 pass.set_pipeline(&pipeline);
425 pass.set_bind_group(0, &camera_bg, &[]);
426 pass.set_bind_group(2, &material_bg, &[]);
427 for (offset, vertex_buffer, index_buffer, index_format, index_count) in &draws {
428 pass.set_bind_group(1, &model_bg, &[*offset]);
429 pass.set_vertex_buffer(0, vertex_buffer, 0);
430 pass.set_index_buffer(index_buffer, 0, *index_format);
431 pass.draw_indexed(0..*index_count, 0, 0..1);
432 }
433}
434
435impl Lane for WireframeLane {
436 fn strategy_name(&self) -> &'static str {
437 "Wireframe"
438 }
439
440 fn lane_kind(&self) -> LaneKind {
441 LaneKind::Render
442 }
443
444 fn on_initialize(&self, ctx: &mut LaneContext) -> Result<(), LaneError> {
445 let device = ctx
446 .get::<Arc<dyn khora_core::renderer::GraphicsDevice>>()
447 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
448 .clone();
449 let pipeline_system = ctx
450 .get::<Arc<dyn khora_core::renderer::traits::PipelineSystem>>()
451 .ok_or(LaneError::missing("Arc<dyn PipelineSystem>"))?
452 .clone();
453 init_gpu_resources(self, device.as_ref(), pipeline_system.as_ref())
454 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))
455 }
456
457 fn execute(&self, ctx: &mut LaneContext) -> Result<(), LaneError> {
458 let config = ctx
461 .get::<SharedWireframeConfig>()
462 .and_then(|cfg| cfg.lock().ok().map(|c| c.clone()));
463 let Some(config) = config else {
464 return Ok(());
465 };
466 if !config.enabled {
467 return Ok(());
468 }
469
470 let Some(render_world) = ctx.get::<Ref<RenderWorld>>() else {
471 return Ok(());
472 };
473 let render_world = render_world.get();
474 let Some(view) = render_world.views.first() else {
475 return Ok(());
476 };
477 let view = view.clone();
478
479 let device = ctx
480 .get::<Arc<dyn khora_core::renderer::GraphicsDevice>>()
481 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
482 .clone();
483 let gpu_meshes = ctx
484 .get::<Arc<RwLock<Assets<GpuMesh>>>>()
485 .ok_or(LaneError::missing("Arc<RwLock<Assets<GpuMesh>>>"))?
486 .clone();
487 let encoder = ctx
488 .get::<Slot<dyn CommandEncoder>>()
489 .ok_or(LaneError::missing("Slot<dyn CommandEncoder>"))?
490 .get();
491 let color_target = ctx
492 .get::<khora_core::lane::ColorTarget>()
493 .ok_or(LaneError::missing("ColorTarget"))?
494 .0;
495 let Some(depth_target) = ctx.get::<khora_core::lane::DepthTarget>().map(|d| d.0) else {
498 return Ok(());
499 };
500
501 render_wireframe(
502 self,
503 device.as_ref(),
504 encoder,
505 color_target,
506 depth_target,
507 &view,
508 render_world,
509 gpu_meshes.as_ref(),
510 &config,
511 );
512 Ok(())
513 }
514
515 fn as_any(&self) -> &dyn std::any::Any {
516 self
517 }
518
519 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
520 self
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527
528 #[test]
529 fn wireframe_lane_strategy_name() {
530 let lane = WireframeLane::default();
531 assert_eq!(lane.strategy_name(), "Wireframe");
532 assert_eq!(lane.lane_kind(), LaneKind::Render);
533 }
534
535 #[test]
536 fn wireframe_material_uniforms_is_std140_sized() {
537 assert_eq!(std::mem::size_of::<WireframeMaterialUniforms>(), 32);
538 assert_eq!(std::mem::align_of::<WireframeMaterialUniforms>(), 16);
539 }
540}