1use khora_core::lane::{Lane, LaneContext, LaneError, LaneKind, Ref, Slot};
34use khora_core::renderer::api::command::{BindGroupId, BindGroupLayoutId};
35use khora_core::renderer::api::ibl::IblGpuBindings;
36use khora_core::renderer::api::pipeline::RenderPipelineId;
37use khora_core::renderer::api::resource::BufferId;
38use khora_core::renderer::traits::CommandEncoder;
39use khora_data::render::RenderWorld;
40use std::sync::{Arc, OnceLock};
41
42const SKY_UNIFORM_LAYOUT_LABEL: &str = "skybox_uniform_layout";
44const SKY_ENV_LAYOUT_LABEL: &str = "skybox_env_layout";
46
47#[repr(C, align(16))]
51#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
52struct SkyUniforms {
53 inv_view_proj: [[f32; 4]; 4],
54 camera_pos: [f32; 4],
55}
56
57#[derive(Debug, Default)]
64pub struct SkyboxLane {
65 pipeline: OnceLock<RenderPipelineId>,
66 sky_layout: OnceLock<BindGroupLayoutId>,
67 env_layout: OnceLock<BindGroupLayoutId>,
68 sky_buffer: OnceLock<BufferId>,
69 sky_bind_group: OnceLock<BindGroupId>,
70 env_bind_group: OnceLock<BindGroupId>,
71}
72
73fn init_gpu_resources(
76 lane: &SkyboxLane,
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::resource::{BufferDescriptor, BufferUsage};
84 use std::borrow::Cow;
85
86 let sky_layout = pipeline_system.inline_layout(
87 device,
88 SKY_UNIFORM_LAYOUT_LABEL,
89 &sky_uniform_layout_entries(),
90 )?;
91 let env_layout =
92 pipeline_system.inline_layout(device, SKY_ENV_LAYOUT_LABEL, &env_layout_entries())?;
93
94 let sky_buffer = device
95 .create_buffer(&BufferDescriptor {
96 label: Some(Cow::Borrowed("skybox_uniform_ubo")),
97 size: std::mem::size_of::<SkyUniforms>() as u64,
98 usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
99 mapped_at_creation: false,
100 })
101 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
102
103 let sky_bind_group = device
104 .create_bind_group(&BindGroupDescriptor {
105 label: Some("skybox_uniform_bg"),
106 layout: sky_layout,
107 entries: &[BindGroupEntry {
108 binding: 0,
109 resource: BindingResource::Buffer(BufferBinding {
110 buffer: sky_buffer,
111 offset: 0,
112 size: None,
113 }),
114 _phantom: std::marker::PhantomData,
115 }],
116 })
117 .map_err(khora_core::renderer::error::RenderError::ResourceError)?;
118
119 let pipeline_id = pipeline_system.pipeline(device, &skybox_pipeline_spec(device))?;
120
121 let _ = lane.sky_layout.set(sky_layout);
122 let _ = lane.env_layout.set(env_layout);
123 let _ = lane.sky_buffer.set(sky_buffer);
124 let _ = lane.sky_bind_group.set(sky_bind_group);
125 let _ = lane.pipeline.set(pipeline_id);
126 Ok(())
127}
128
129fn sky_uniform_layout_entries() -> Vec<khora_core::renderer::api::command::BindGroupLayoutEntry> {
132 use khora_core::renderer::api::command::{
133 BindGroupLayoutEntry, BindingType, BufferBindingType,
134 };
135 use khora_core::renderer::api::util::ShaderStageFlags;
136 vec![BindGroupLayoutEntry {
137 binding: 0,
138 visibility: ShaderStageFlags::FRAGMENT,
139 ty: BindingType::Buffer {
140 ty: BufferBindingType::Uniform,
141 has_dynamic_offset: false,
142 min_binding_size: None,
143 },
144 }]
145}
146
147fn env_layout_entries() -> Vec<khora_core::renderer::api::command::BindGroupLayoutEntry> {
149 use khora_core::renderer::api::command::{
150 BindGroupLayoutEntry, BindingType, SamplerBindingType, TextureSampleType,
151 TextureViewDimension,
152 };
153 use khora_core::renderer::api::util::ShaderStageFlags;
154 vec![
155 BindGroupLayoutEntry {
156 binding: 0,
157 visibility: ShaderStageFlags::FRAGMENT,
158 ty: BindingType::Texture {
159 sample_type: TextureSampleType::Float { filterable: true },
160 view_dimension: TextureViewDimension::Cube,
161 multisampled: false,
162 },
163 },
164 BindGroupLayoutEntry {
165 binding: 1,
166 visibility: ShaderStageFlags::FRAGMENT,
167 ty: BindingType::Sampler(SamplerBindingType::Filtering),
168 },
169 ]
170}
171
172fn skybox_pipeline_spec(
176 device: &dyn khora_core::renderer::GraphicsDevice,
177) -> khora_core::renderer::api::pipeline::PipelineSpec {
178 use khora_core::renderer::api::pipeline::enums::{CompareFunction, PrimitiveTopology};
179 use khora_core::renderer::api::pipeline::state::{
180 ColorWrites, DepthBiasState, StencilFaceState,
181 };
182 use khora_core::renderer::api::pipeline::{
183 ColorTargetStateDescriptor, DepthStencilStateDescriptor, LayoutSpec,
184 MultisampleStateDescriptor, PipelineSpec, PrimitiveStateDescriptor, ShaderVariantKey,
185 };
186 use khora_core::renderer::api::util::{SampleCount, TextureFormat};
187 use std::borrow::Cow;
188
189 PipelineSpec {
190 label: "Skybox Pipeline",
191 shader: "khora::pipelines::skybox",
192 variant: ShaderVariantKey::empty(),
193 bind_group_layouts: vec![
194 LayoutSpec::Inline {
195 label: SKY_UNIFORM_LAYOUT_LABEL,
196 entries: Cow::Owned(sky_uniform_layout_entries()),
197 },
198 LayoutSpec::Inline {
199 label: SKY_ENV_LAYOUT_LABEL,
200 entries: Cow::Owned(env_layout_entries()),
201 },
202 ],
203 vertex_buffers: vec![],
204 vs_entry: "vs_main",
205 fs_entry: Some("fs_main"),
206 primitive: PrimitiveStateDescriptor {
207 topology: PrimitiveTopology::TriangleList,
208 ..Default::default()
209 },
210 depth_stencil: Some(DepthStencilStateDescriptor {
214 format: TextureFormat::Depth32Float,
215 depth_write_enabled: false,
216 depth_compare: CompareFunction::LessEqual,
217 stencil_front: StencilFaceState::default(),
218 stencil_back: StencilFaceState::default(),
219 stencil_read_mask: 0,
220 stencil_write_mask: 0,
221 bias: DepthBiasState::default(),
222 }),
223 color_targets: vec![ColorTargetStateDescriptor {
224 format: device
225 .get_surface_format()
226 .unwrap_or(TextureFormat::Rgba8UnormSrgb),
227 blend: None,
228 write_mask: ColorWrites::ALL,
229 }],
230 multisample: MultisampleStateDescriptor {
231 count: SampleCount::X1,
232 mask: !0,
233 alpha_to_coverage_enabled: false,
234 },
235 }
236}
237
238fn env_bind_group(
242 lane: &SkyboxLane,
243 device: &dyn khora_core::renderer::GraphicsDevice,
244 ibl: &IblGpuBindings,
245) -> Option<BindGroupId> {
246 if let Some(bg) = lane.env_bind_group.get().copied() {
247 return Some(bg);
248 }
249 use khora_core::renderer::api::command::{
250 BindGroupDescriptor, BindGroupEntry, BindingResource,
251 };
252 let layout = lane.env_layout.get().copied()?;
253 let bg = device
254 .create_bind_group(&BindGroupDescriptor {
255 label: Some("skybox_env_bg"),
256 layout,
257 entries: &[
258 BindGroupEntry {
259 binding: 0,
260 resource: BindingResource::TextureView(ibl.env_cube),
261 _phantom: std::marker::PhantomData,
262 },
263 BindGroupEntry {
264 binding: 1,
265 resource: BindingResource::Sampler(ibl.sampler),
266 _phantom: std::marker::PhantomData,
267 },
268 ],
269 })
270 .map_err(|e| log::error!("SkyboxLane: env bind group creation failed: {e:?}"))
271 .ok()?;
272 let _ = lane.env_bind_group.set(bg);
273 Some(bg)
274}
275
276#[allow(clippy::too_many_arguments)]
277fn render_skybox(
278 lane: &SkyboxLane,
279 device: &dyn khora_core::renderer::GraphicsDevice,
280 encoder: &mut dyn CommandEncoder,
281 color_target: khora_core::renderer::api::resource::TextureViewId,
282 depth_target: khora_core::renderer::api::resource::TextureViewId,
283 view: &khora_data::render::ExtractedView,
284 ibl: &IblGpuBindings,
285) {
286 use khora_core::renderer::api::command::{
287 LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment,
288 RenderPassDescriptor, StoreOp,
289 };
290
291 let (Some(pipeline), Some(sky_buffer), Some(sky_bg)) = (
292 lane.pipeline.get().copied(),
293 lane.sky_buffer.get().copied(),
294 lane.sky_bind_group.get().copied(),
295 ) else {
296 log::warn!("SkyboxLane: GPU resources not initialized, skipping");
297 return;
298 };
299 let Some(env_bg) = env_bind_group(lane, device, ibl) else {
300 return;
301 };
302
303 let Some(inv_view_proj) = view.view_proj.inverse() else {
306 return;
307 };
308 let uniforms = SkyUniforms {
309 inv_view_proj: inv_view_proj.to_cols_array_2d(),
310 camera_pos: [view.position.x, view.position.y, view.position.z, 1.0],
311 };
312 if let Err(e) = device.write_buffer(sky_buffer, 0, bytemuck::bytes_of(&uniforms)) {
313 log::error!("SkyboxLane: uniform buffer write failed: {:?}", e);
314 return;
315 }
316
317 let color_attachment = RenderPassColorAttachment {
321 view: &color_target,
322 resolve_target: None,
323 ops: Operations {
324 load: LoadOp::Load,
325 store: StoreOp::Store,
326 },
327 base_array_layer: 0,
328 base_mip_level: 0,
329 };
330 let pass_desc = RenderPassDescriptor {
331 label: Some("Skybox Pass"),
332 color_attachments: &[color_attachment],
333 depth_stencil_attachment: Some(RenderPassDepthStencilAttachment {
334 view: &depth_target,
335 depth_ops: Some(Operations {
336 load: LoadOp::Load,
337 store: StoreOp::Store,
338 }),
339 stencil_ops: None,
340 base_array_layer: 0,
341 }),
342 };
343
344 let mut pass = encoder.begin_render_pass(&pass_desc);
345 pass.set_pipeline(&pipeline);
346 pass.set_bind_group(0, &sky_bg, &[]);
347 pass.set_bind_group(1, &env_bg, &[]);
348 pass.draw(0..3, 0..1);
351}
352
353impl Lane for SkyboxLane {
354 fn strategy_name(&self) -> &'static str {
355 "Skybox"
356 }
357
358 fn lane_kind(&self) -> LaneKind {
359 LaneKind::Render
360 }
361
362 fn on_initialize(&self, ctx: &mut LaneContext) -> Result<(), LaneError> {
363 let device = ctx
364 .get::<Arc<dyn khora_core::renderer::GraphicsDevice>>()
365 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
366 .clone();
367 let pipeline_system = ctx
368 .get::<Arc<dyn khora_core::renderer::traits::PipelineSystem>>()
369 .ok_or(LaneError::missing("Arc<dyn PipelineSystem>"))?
370 .clone();
371 init_gpu_resources(self, device.as_ref(), pipeline_system.as_ref())
372 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))
373 }
374
375 fn execute(&self, ctx: &mut LaneContext) -> Result<(), LaneError> {
376 let Some(ibl) = ctx.get::<IblGpuBindings>().copied() else {
379 return Ok(());
380 };
381
382 let Some(render_world) = ctx.get::<Ref<RenderWorld>>() else {
385 return Ok(());
386 };
387 let render_world = render_world.get();
388 let Some(view) = render_world.views.first() else {
389 return Ok(());
390 };
391 let view = view.clone();
392
393 let device = ctx
394 .get::<Arc<dyn khora_core::renderer::GraphicsDevice>>()
395 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
396 .clone();
397 let encoder = ctx
398 .get::<Slot<dyn CommandEncoder>>()
399 .ok_or(LaneError::missing("Slot<dyn CommandEncoder>"))?
400 .get();
401 let color_target = ctx
402 .get::<khora_core::lane::ColorTarget>()
403 .ok_or(LaneError::missing("ColorTarget"))?
404 .0;
405 let Some(depth_target) = ctx.get::<khora_core::lane::DepthTarget>().map(|d| d.0) else {
408 return Ok(());
409 };
410
411 render_skybox(
412 self,
413 device.as_ref(),
414 encoder,
415 color_target,
416 depth_target,
417 &view,
418 &ibl,
419 );
420 Ok(())
421 }
422
423 fn as_any(&self) -> &dyn std::any::Any {
424 self
425 }
426
427 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
428 self
429 }
430}
431
432#[cfg(test)]
433mod tests {
434 use super::*;
435
436 #[test]
437 fn skybox_lane_strategy_name() {
438 let lane = SkyboxLane::default();
439 assert_eq!(lane.strategy_name(), "Skybox");
440 assert_eq!(lane.lane_kind(), LaneKind::Render);
441 }
442
443 #[test]
444 fn sky_uniforms_are_std140_sized() {
445 assert_eq!(std::mem::size_of::<SkyUniforms>(), 80);
447 assert_eq!(std::mem::align_of::<SkyUniforms>(), 16);
448 }
449}