1use khora_core::lane::{Lane, LaneContext, LaneError, LaneKind, Ref, Slot};
32use khora_core::renderer::api::command::{BindGroupId, BindGroupLayoutId};
33use khora_core::renderer::api::pipeline::RenderPipelineId;
34use khora_core::renderer::api::resource::BufferId;
35use khora_core::renderer::traits::CommandEncoder;
36use khora_data::render::{GridConfig, RenderWorld};
37use std::sync::{Arc, Mutex, OnceLock};
38
39pub type SharedGridConfig = Arc<Mutex<GridConfig>>;
41
42#[derive(Debug, Default)]
44pub struct GridLane {
45 pipeline: OnceLock<RenderPipelineId>,
46 camera_layout: OnceLock<BindGroupLayoutId>,
47 camera_buffer: OnceLock<BufferId>,
48 camera_bind_group: OnceLock<BindGroupId>,
49}
50
51fn init_gpu_resources(
54 lane: &GridLane,
55 device: &dyn khora_core::renderer::GraphicsDevice,
56 pipeline_system: &dyn khora_core::renderer::traits::PipelineSystem,
57) -> Result<(), khora_core::renderer::error::RenderError> {
58 use khora_core::renderer::api::{
59 command::{BindGroupDescriptor, BindGroupEntry, BindingResource, BufferBinding},
60 resource::{BufferDescriptor, BufferUsage, CameraUniformData},
61 };
62 use std::borrow::Cow;
63
64 let camera_layout = pipeline_system.inline_layout(
68 device,
69 GRID_CAMERA_LAYOUT_LABEL,
70 &grid_camera_layout_entries(),
71 )?;
72
73 let camera_buffer = device
74 .create_buffer(&BufferDescriptor {
75 label: Some(Cow::Borrowed("grid_camera_ubo")),
76 size: std::mem::size_of::<CameraUniformData>() as u64,
77 usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
78 mapped_at_creation: false,
79 })
80 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
81
82 let camera_bind_group = device
83 .create_bind_group(&BindGroupDescriptor {
84 label: Some("grid_camera_bg"),
85 layout: camera_layout,
86 entries: &[BindGroupEntry {
87 binding: 0,
88 resource: BindingResource::Buffer(BufferBinding {
89 buffer: camera_buffer,
90 offset: 0,
91 size: None,
92 }),
93 _phantom: std::marker::PhantomData,
94 }],
95 })
96 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
97
98 let pipeline_id = pipeline_system.pipeline(device, &grid_pipeline_spec(device))?;
100
101 let _ = lane.camera_layout.set(camera_layout);
102 let _ = lane.camera_buffer.set(camera_buffer);
103 let _ = lane.camera_bind_group.set(camera_bind_group);
104 let _ = lane.pipeline.set(pipeline_id);
105 Ok(())
106}
107
108const GRID_CAMERA_LAYOUT_LABEL: &str = "grid_camera_layout";
110
111fn grid_camera_layout_entries() -> Vec<khora_core::renderer::api::command::BindGroupLayoutEntry> {
113 use khora_core::renderer::api::command::{
114 BindGroupLayoutEntry, BindingType, BufferBindingType,
115 };
116 use khora_core::renderer::api::util::ShaderStageFlags;
117 vec![BindGroupLayoutEntry {
118 binding: 0,
119 visibility: ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
120 ty: BindingType::Buffer {
121 ty: BufferBindingType::Uniform,
122 has_dynamic_offset: false,
123 min_binding_size: None,
124 },
125 }]
126}
127
128fn grid_pipeline_spec(
131 device: &dyn khora_core::renderer::GraphicsDevice,
132) -> khora_core::renderer::api::pipeline::PipelineSpec {
133 use khora_core::renderer::api::pipeline::enums::{
134 BlendFactor, BlendOperation, CompareFunction, PrimitiveTopology,
135 };
136 use khora_core::renderer::api::pipeline::state::{
137 BlendComponentDescriptor, BlendStateDescriptor, ColorWrites, DepthBiasState,
138 StencilFaceState,
139 };
140 use khora_core::renderer::api::pipeline::{
141 ColorTargetStateDescriptor, DepthStencilStateDescriptor, LayoutSpec,
142 MultisampleStateDescriptor, PipelineSpec, PrimitiveStateDescriptor, ShaderVariantKey,
143 };
144 use khora_core::renderer::api::util::{SampleCount, TextureFormat};
145 use std::borrow::Cow;
146
147 let blend = BlendStateDescriptor {
149 color: BlendComponentDescriptor {
150 src_factor: BlendFactor::SrcAlpha,
151 dst_factor: BlendFactor::OneMinusSrcAlpha,
152 operation: BlendOperation::Add,
153 },
154 alpha: BlendComponentDescriptor {
155 src_factor: BlendFactor::One,
156 dst_factor: BlendFactor::OneMinusSrcAlpha,
157 operation: BlendOperation::Add,
158 },
159 };
160
161 PipelineSpec {
162 label: "Grid Pipeline",
163 shader: "khora::pipelines::grid",
164 variant: ShaderVariantKey::empty(),
165 bind_group_layouts: vec![LayoutSpec::Inline {
166 label: GRID_CAMERA_LAYOUT_LABEL,
167 entries: Cow::Owned(grid_camera_layout_entries()),
168 }],
169 vertex_buffers: vec![],
170 vs_entry: "vs_main",
171 fs_entry: Some("fs_main"),
172 primitive: PrimitiveStateDescriptor {
173 topology: PrimitiveTopology::TriangleList,
174 ..Default::default()
175 },
176 depth_stencil: Some(DepthStencilStateDescriptor {
180 format: TextureFormat::Depth32Float,
181 depth_write_enabled: true,
182 depth_compare: CompareFunction::LessEqual,
183 stencil_front: StencilFaceState::default(),
184 stencil_back: StencilFaceState::default(),
185 stencil_read_mask: 0,
186 stencil_write_mask: 0,
187 bias: DepthBiasState::default(),
188 }),
189 color_targets: vec![ColorTargetStateDescriptor {
190 format: device
191 .get_surface_format()
192 .unwrap_or(TextureFormat::Rgba8UnormSrgb),
193 blend: Some(blend),
194 write_mask: ColorWrites::ALL,
195 }],
196 multisample: MultisampleStateDescriptor {
197 count: SampleCount::X1,
198 mask: !0,
199 alpha_to_coverage_enabled: false,
200 },
201 }
202}
203
204fn render_grid(
205 lane: &GridLane,
206 device: &dyn khora_core::renderer::GraphicsDevice,
207 encoder: &mut dyn CommandEncoder,
208 color_target: khora_core::renderer::api::resource::TextureViewId,
209 depth_target: khora_core::renderer::api::resource::TextureViewId,
210 view: &khora_data::render::ExtractedView,
211) {
212 use khora_core::renderer::api::command::{
213 LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment,
214 RenderPassDescriptor, StoreOp,
215 };
216 use khora_core::renderer::api::resource::CameraUniformData;
217
218 let (Some(pipeline), Some(camera_buffer), Some(camera_bg)) = (
219 lane.pipeline.get().copied(),
220 lane.camera_buffer.get().copied(),
221 lane.camera_bind_group.get().copied(),
222 ) else {
223 log::warn!("GridLane: GPU resources not initialized, skipping");
224 return;
225 };
226
227 let camera_uniforms = CameraUniformData {
229 view_projection: view.view_proj.to_cols_array_2d(),
230 camera_position: [view.position.x, view.position.y, view.position.z, 1.0],
231 };
232 if let Err(e) = device.write_buffer(camera_buffer, 0, bytemuck::bytes_of(&camera_uniforms)) {
233 log::error!("GridLane: camera buffer write failed: {:?}", e);
234 return;
235 }
236
237 let color_attachment = RenderPassColorAttachment {
239 view: &color_target,
240 resolve_target: None,
241 ops: Operations {
242 load: LoadOp::Load,
243 store: StoreOp::Store,
244 },
245 base_array_layer: 0,
246 base_mip_level: 0,
247 };
248 let pass_desc = RenderPassDescriptor {
249 label: Some("Grid Overlay Pass"),
250 color_attachments: &[color_attachment],
251 depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
252 view: &depth_target,
253 depth_ops: Some(Operations {
254 load: LoadOp::Load,
255 store: StoreOp::Store,
256 }),
257 stencil_ops: None,
258 base_array_layer: 0,
259 }),
260 };
261
262 let mut pass = encoder.begin_render_pass(&pass_desc);
263 pass.set_pipeline(&pipeline);
264 pass.set_bind_group(0, &camera_bg, &[]);
265 pass.draw(0..6, 0..1);
268}
269
270impl Lane for GridLane {
271 fn strategy_name(&self) -> &'static str {
272 "Grid"
273 }
274
275 fn lane_kind(&self) -> LaneKind {
276 LaneKind::Render
277 }
278
279 fn on_initialize(&self, ctx: &mut LaneContext) -> Result<(), LaneError> {
280 let device = ctx
281 .get::<Arc<dyn khora_core::renderer::GraphicsDevice>>()
282 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
283 .clone();
284 let pipeline_system = ctx
285 .get::<Arc<dyn khora_core::renderer::traits::PipelineSystem>>()
286 .ok_or(LaneError::missing("Arc<dyn PipelineSystem>"))?
287 .clone();
288 init_gpu_resources(self, device.as_ref(), pipeline_system.as_ref())
289 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))
290 }
291
292 fn execute(&self, ctx: &mut LaneContext) -> Result<(), LaneError> {
293 let enabled = ctx
296 .get::<SharedGridConfig>()
297 .and_then(|cfg| cfg.lock().ok().map(|c| c.enabled))
298 .unwrap_or(false);
299 if !enabled {
300 return Ok(());
301 }
302
303 let Some(render_world) = ctx.get::<Ref<RenderWorld>>() else {
307 return Ok(());
308 };
309 let render_world = render_world.get();
310 let Some(view) = render_world.views.first() else {
311 return Ok(());
312 };
313 let view = view.clone();
314
315 let device = ctx
316 .get::<Arc<dyn khora_core::renderer::GraphicsDevice>>()
317 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
318 .clone();
319 let encoder = ctx
320 .get::<Slot<dyn CommandEncoder>>()
321 .ok_or(LaneError::missing("Slot<dyn CommandEncoder>"))?
322 .get();
323 let color_target = ctx
324 .get::<khora_core::lane::ColorTarget>()
325 .ok_or(LaneError::missing("ColorTarget"))?
326 .0;
327 let Some(depth_target) = ctx.get::<khora_core::lane::DepthTarget>().map(|d| d.0) else {
330 return Ok(());
331 };
332
333 render_grid(
334 self,
335 device.as_ref(),
336 encoder,
337 color_target,
338 depth_target,
339 &view,
340 );
341 Ok(())
342 }
343
344 fn as_any(&self) -> &dyn std::any::Any {
345 self
346 }
347
348 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
349 self
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn grid_lane_strategy_name() {
359 let lane = GridLane::default();
360 assert_eq!(lane.strategy_name(), "Grid");
361 assert_eq!(lane.lane_kind(), LaneKind::Render);
362 }
363}