1use khora_core::lane::{Lane, LaneContext, LaneError, LaneKind, Ref, Slot};
28use khora_core::renderer::api::command::{BindGroupId, BindGroupLayoutId};
29use khora_core::renderer::api::pipeline::RenderPipelineId;
30use khora_core::renderer::api::resource::BufferId;
31use khora_core::renderer::traits::CommandEncoder;
32use khora_core::ui::editor::GizmoLineInstance;
33use khora_data::render::{GizmoFrame, RenderWorld};
34use std::sync::{Arc, Mutex, OnceLock};
35
36const GIZMO_CAPACITY: usize = 4096;
38
39pub type SharedGizmoFrame = Arc<Mutex<GizmoFrame>>;
41
42#[derive(Debug)]
44pub struct GizmoLane {
45 pipeline: OnceLock<RenderPipelineId>,
46 camera_layout: OnceLock<BindGroupLayoutId>,
47 storage_layout: OnceLock<BindGroupLayoutId>,
48 camera_buffer: OnceLock<BufferId>,
49 storage_buffer: OnceLock<BufferId>,
50 camera_bind_group: OnceLock<BindGroupId>,
51 storage_bind_group: OnceLock<BindGroupId>,
52 capacity: usize,
54}
55
56impl Default for GizmoLane {
57 fn default() -> Self {
58 Self {
59 pipeline: OnceLock::new(),
60 camera_layout: OnceLock::new(),
61 storage_layout: OnceLock::new(),
62 camera_buffer: OnceLock::new(),
63 storage_buffer: OnceLock::new(),
64 camera_bind_group: OnceLock::new(),
65 storage_bind_group: OnceLock::new(),
66 capacity: GIZMO_CAPACITY,
67 }
68 }
69}
70
71fn init_gpu_resources(
74 lane: &GizmoLane,
75 device: &dyn khora_core::renderer::GraphicsDevice,
76 pipeline_system: &dyn khora_core::renderer::traits::PipelineSystem,
77) -> Result<(), khora_core::renderer::error::RenderError> {
78 use khora_core::renderer::api::{
79 command::{BindGroupDescriptor, BindGroupEntry, BindingResource, BufferBinding},
80 resource::{BufferDescriptor, BufferUsage},
81 };
82 use std::borrow::Cow;
83
84 let camera_layout = pipeline_system.inline_layout(
87 device,
88 GIZMO_CAMERA_LAYOUT_LABEL,
89 &gizmo_camera_layout_entries(),
90 )?;
91 let storage_layout = pipeline_system.inline_layout(
92 device,
93 GIZMO_STORAGE_LAYOUT_LABEL,
94 &gizmo_storage_layout_entries(),
95 )?;
96
97 let camera_buffer = device
98 .create_buffer(&BufferDescriptor {
99 label: Some(Cow::Borrowed("gizmo_camera_ubo")),
100 size: std::mem::size_of::<khora_core::renderer::api::resource::CameraUniformData>()
101 as u64,
102 usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
103 mapped_at_creation: false,
104 })
105 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
106 let storage_size = (lane.capacity * std::mem::size_of::<GizmoLineInstance>()) as u64;
107 let storage_buffer = device
108 .create_buffer(&BufferDescriptor {
109 label: Some(Cow::Borrowed("gizmo_storage_buffer")),
110 size: storage_size,
111 usage: BufferUsage::STORAGE | BufferUsage::COPY_DST,
112 mapped_at_creation: false,
113 })
114 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
115
116 let camera_bind_group = device
117 .create_bind_group(&BindGroupDescriptor {
118 label: Some("gizmo_camera_bg"),
119 layout: camera_layout,
120 entries: &[BindGroupEntry {
121 binding: 0,
122 resource: BindingResource::Buffer(BufferBinding {
123 buffer: camera_buffer,
124 offset: 0,
125 size: None,
126 }),
127 _phantom: std::marker::PhantomData,
128 }],
129 })
130 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
131 let storage_bind_group = device
132 .create_bind_group(&BindGroupDescriptor {
133 label: Some("gizmo_storage_bg"),
134 layout: storage_layout,
135 entries: &[BindGroupEntry {
136 binding: 0,
137 resource: BindingResource::Buffer(BufferBinding {
138 buffer: storage_buffer,
139 offset: 0,
140 size: None,
141 }),
142 _phantom: std::marker::PhantomData,
143 }],
144 })
145 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
146
147 let pipeline_id = pipeline_system.pipeline(device, &gizmo_pipeline_spec(device))?;
150
151 let _ = lane.camera_layout.set(camera_layout);
152 let _ = lane.storage_layout.set(storage_layout);
153 let _ = lane.camera_buffer.set(camera_buffer);
154 let _ = lane.storage_buffer.set(storage_buffer);
155 let _ = lane.camera_bind_group.set(camera_bind_group);
156 let _ = lane.storage_bind_group.set(storage_bind_group);
157 let _ = lane.pipeline.set(pipeline_id);
158 Ok(())
159}
160
161const GIZMO_CAMERA_LAYOUT_LABEL: &str = "gizmo_camera_layout";
163const GIZMO_STORAGE_LAYOUT_LABEL: &str = "gizmo_storage_layout";
164
165fn gizmo_camera_layout_entries() -> Vec<khora_core::renderer::api::command::BindGroupLayoutEntry> {
167 use khora_core::renderer::api::command::{
168 BindGroupLayoutEntry, BindingType, BufferBindingType,
169 };
170 use khora_core::renderer::api::util::ShaderStageFlags;
171 vec![BindGroupLayoutEntry {
172 binding: 0,
173 visibility: ShaderStageFlags::VERTEX,
174 ty: BindingType::Buffer {
175 ty: BufferBindingType::Uniform,
176 has_dynamic_offset: false,
177 min_binding_size: None,
178 },
179 }]
180}
181
182fn gizmo_storage_layout_entries() -> Vec<khora_core::renderer::api::command::BindGroupLayoutEntry> {
184 use khora_core::renderer::api::command::{
185 BindGroupLayoutEntry, BindingType, BufferBindingType,
186 };
187 use khora_core::renderer::api::util::ShaderStageFlags;
188 vec![BindGroupLayoutEntry {
189 binding: 0,
190 visibility: ShaderStageFlags::VERTEX,
191 ty: BindingType::Buffer {
192 ty: BufferBindingType::Storage { read_only: true },
193 has_dynamic_offset: false,
194 min_binding_size: None,
195 },
196 }]
197}
198
199fn gizmo_pipeline_spec(
202 device: &dyn khora_core::renderer::GraphicsDevice,
203) -> khora_core::renderer::api::pipeline::PipelineSpec {
204 use khora_core::renderer::api::pipeline::enums::{
205 BlendFactor, BlendOperation, PrimitiveTopology,
206 };
207 use khora_core::renderer::api::pipeline::state::{
208 BlendComponentDescriptor, BlendStateDescriptor, ColorWrites,
209 };
210 use khora_core::renderer::api::pipeline::{
211 ColorTargetStateDescriptor, LayoutSpec, MultisampleStateDescriptor, PipelineSpec,
212 PrimitiveStateDescriptor, ShaderVariantKey,
213 };
214 use khora_core::renderer::api::util::{SampleCount, TextureFormat};
215 use std::borrow::Cow;
216
217 let blend = BlendStateDescriptor {
221 color: BlendComponentDescriptor {
222 src_factor: BlendFactor::SrcAlpha,
223 dst_factor: BlendFactor::OneMinusSrcAlpha,
224 operation: BlendOperation::Add,
225 },
226 alpha: BlendComponentDescriptor {
227 src_factor: BlendFactor::One,
228 dst_factor: BlendFactor::OneMinusSrcAlpha,
229 operation: BlendOperation::Add,
230 },
231 };
232
233 PipelineSpec {
234 label: "Gizmo Pipeline",
235 shader: "khora::pipelines::gizmo",
236 variant: ShaderVariantKey::empty(),
237 bind_group_layouts: vec![
238 LayoutSpec::Inline {
239 label: GIZMO_CAMERA_LAYOUT_LABEL,
240 entries: Cow::Owned(gizmo_camera_layout_entries()),
241 },
242 LayoutSpec::Inline {
243 label: GIZMO_STORAGE_LAYOUT_LABEL,
244 entries: Cow::Owned(gizmo_storage_layout_entries()),
245 },
246 ],
247 vertex_buffers: vec![],
248 vs_entry: "vs_main",
249 fs_entry: Some("fs_main"),
250 primitive: PrimitiveStateDescriptor {
251 topology: PrimitiveTopology::LineList,
252 ..Default::default()
253 },
254 depth_stencil: None,
255 color_targets: vec![ColorTargetStateDescriptor {
256 format: device
257 .get_surface_format()
258 .unwrap_or(TextureFormat::Rgba8UnormSrgb),
259 blend: Some(blend),
260 write_mask: ColorWrites::ALL,
261 }],
262 multisample: MultisampleStateDescriptor {
263 count: SampleCount::X1,
264 mask: !0,
265 alpha_to_coverage_enabled: false,
266 },
267 }
268}
269
270fn render_gizmos(
271 lane: &GizmoLane,
272 device: &dyn khora_core::renderer::GraphicsDevice,
273 encoder: &mut dyn CommandEncoder,
274 color_target: khora_core::renderer::api::resource::TextureViewId,
275 view: &khora_data::render::ExtractedView,
276 lines: &[GizmoLineInstance],
277) {
278 use khora_core::renderer::api::command::{
279 LoadOp, Operations, RenderPassColorAttachment, RenderPassDescriptor, StoreOp,
280 };
281 use khora_core::renderer::api::resource::CameraUniformData;
282
283 let line_count = lines.len().min(lane.capacity);
284 if line_count == 0 {
285 return;
286 }
287
288 let (
289 Some(pipeline),
290 Some(camera_buffer),
291 Some(storage_buffer),
292 Some(camera_bg),
293 Some(storage_bg),
294 ) = (
295 lane.pipeline.get().copied(),
296 lane.camera_buffer.get().copied(),
297 lane.storage_buffer.get().copied(),
298 lane.camera_bind_group.get().copied(),
299 lane.storage_bind_group.get().copied(),
300 )
301 else {
302 log::warn!("GizmoLane: GPU resources not initialized, skipping");
303 return;
304 };
305
306 let camera_uniforms = CameraUniformData {
309 view_projection: view.view_proj.to_cols_array_2d(),
310 camera_position: [view.position.x, view.position.y, view.position.z, 1.0],
311 };
312 if let Err(e) = device.write_buffer(camera_buffer, 0, bytemuck::bytes_of(&camera_uniforms)) {
313 log::error!("GizmoLane: camera buffer write failed: {:?}", e);
314 return;
315 }
316 if let Err(e) = device.write_buffer(
317 storage_buffer,
318 0,
319 bytemuck::cast_slice(&lines[..line_count]),
320 ) {
321 log::error!("GizmoLane: storage buffer write failed: {:?}", e);
322 return;
323 }
324
325 let color_attachment = RenderPassColorAttachment {
327 view: &color_target,
328 resolve_target: None,
329 ops: Operations {
330 load: LoadOp::Load,
331 store: StoreOp::Store,
332 },
333 base_array_layer: 0,
334 base_mip_level: 0,
335 };
336 let pass_desc = RenderPassDescriptor {
337 label: Some("Gizmo Overlay Pass"),
338 color_attachments: &[color_attachment],
339 depth_stencil_attachment: None,
340 };
341
342 let mut pass = encoder.begin_render_pass(&pass_desc);
343 pass.set_pipeline(&pipeline);
344 pass.set_bind_group(0, &camera_bg, &[]);
345 pass.set_bind_group(1, &storage_bg, &[]);
346 pass.draw(0..(line_count as u32 * 2), 0..1);
349}
350
351impl Lane for GizmoLane {
352 fn strategy_name(&self) -> &'static str {
353 "Gizmo"
354 }
355
356 fn lane_kind(&self) -> LaneKind {
357 LaneKind::Render
358 }
359
360 fn on_initialize(&self, ctx: &mut LaneContext) -> Result<(), LaneError> {
361 let device = ctx
362 .get::<Arc<dyn khora_core::renderer::GraphicsDevice>>()
363 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
364 .clone();
365 let pipeline_system = ctx
366 .get::<Arc<dyn khora_core::renderer::traits::PipelineSystem>>()
367 .ok_or(LaneError::missing("Arc<dyn PipelineSystem>"))?
368 .clone();
369 init_gpu_resources(self, device.as_ref(), pipeline_system.as_ref())
370 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))
371 }
372
373 fn execute(&self, ctx: &mut LaneContext) -> Result<(), LaneError> {
374 let Some(shared) = ctx.get::<SharedGizmoFrame>() else {
377 return Ok(());
378 };
379 let lines: Vec<GizmoLineInstance> = match shared.lock() {
380 Ok(frame) => {
381 if frame.lines.is_empty() {
382 return Ok(());
383 }
384 frame.lines.clone()
385 }
386 Err(_) => {
387 log::warn!("GizmoLane: shared GizmoFrame mutex poisoned");
388 return Ok(());
389 }
390 };
391
392 let Some(render_world) = ctx.get::<Ref<RenderWorld>>() else {
396 return Ok(());
397 };
398 let render_world = render_world.get();
399 let Some(view) = render_world.views.first() else {
400 return Ok(());
401 };
402 let view = view.clone();
403
404 let device = ctx
405 .get::<Arc<dyn khora_core::renderer::GraphicsDevice>>()
406 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
407 .clone();
408 let encoder = ctx
409 .get::<Slot<dyn CommandEncoder>>()
410 .ok_or(LaneError::missing("Slot<dyn CommandEncoder>"))?
411 .get();
412 let color_target = ctx
413 .get::<khora_core::lane::ColorTarget>()
414 .ok_or(LaneError::missing("ColorTarget"))?
415 .0;
416
417 render_gizmos(self, device.as_ref(), encoder, color_target, &view, &lines);
418 Ok(())
419 }
420
421 fn as_any(&self) -> &dyn std::any::Any {
422 self
423 }
424
425 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
426 self
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433
434 #[test]
435 fn gizmo_lane_strategy_name() {
436 let lane = GizmoLane::default();
437 assert_eq!(lane.strategy_name(), "Gizmo");
438 assert_eq!(lane.lane_kind(), LaneKind::Render);
439 assert_eq!(lane.capacity, GIZMO_CAPACITY);
440 }
441}