1use std::any::Any;
18use std::borrow::Cow;
19use std::sync::{Arc, Mutex, OnceLock};
20
21use khora_core::lane::{Lane, LaneContext, LaneError, LaneKind, Ref, Slot};
22use khora_core::math::{Mat4, Vec4};
23use khora_core::renderer::api::command::{
24 BindGroupDescriptor, BindGroupEntry, BindGroupId, BindGroupLayoutEntry, BindGroupLayoutId,
25 BindingResource, BindingType, BufferBinding, BufferBindingType, LoadOp, Operations,
26 RenderPassColorAttachment, RenderPassDescriptor, StoreOp,
27};
28use khora_core::renderer::api::pipeline::{
29 ColorTargetStateDescriptor, ColorWrites, MultisampleStateDescriptor, PrimitiveStateDescriptor,
30 PrimitiveTopology, RenderPipelineId,
31};
32use khora_core::renderer::api::resource::{
33 BufferDescriptor, BufferId, BufferUsage, TextureViewDimension, TextureViewId,
34};
35use khora_core::renderer::api::text::TextRenderer;
36use khora_core::renderer::api::util::{SampleCount, ShaderStageFlags, TextureFormat};
37use khora_core::renderer::GraphicsDevice;
38use khora_data::ui::UiScene;
39
40#[repr(C)]
42#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
43struct UiInstanceData {
44 pos: [f32; 2],
45 size: [f32; 2],
46 color: [f32; 4],
47 params: [f32; 4],
48 uv_min: [f32; 2],
49 uv_max: [f32; 2],
50}
51
52pub struct UiRenderLane {
60 pipeline: OnceLock<RenderPipelineId>,
62 global_layout: OnceLock<BindGroupLayoutId>,
64 instance_layout: OnceLock<BindGroupLayoutId>,
66 projection_buffer: OnceLock<BufferId>,
68 instance_buffer: OnceLock<BufferId>,
70 global_bind_group: OnceLock<BindGroupId>,
72 instance_bind_group: OnceLock<BindGroupId>,
74 atlas_layout: OnceLock<BindGroupLayoutId>,
76 sampler: OnceLock<khora_core::renderer::api::resource::SamplerId>,
78 atlas_bind_group: Mutex<Option<(TextureViewId, BindGroupId)>>,
85 max_instances: usize,
87}
88
89impl Default for UiRenderLane {
90 fn default() -> Self {
91 Self {
92 pipeline: OnceLock::new(),
93 global_layout: OnceLock::new(),
94 instance_layout: OnceLock::new(),
95 projection_buffer: OnceLock::new(),
96 instance_buffer: OnceLock::new(),
97 global_bind_group: OnceLock::new(),
98 instance_bind_group: OnceLock::new(),
99 atlas_layout: OnceLock::new(),
100 sampler: OnceLock::new(),
101 atlas_bind_group: Mutex::new(None),
102 max_instances: 1024,
103 }
104 }
105}
106
107impl UiRenderLane {
108 pub fn new() -> Self {
110 Self::default()
111 }
112
113 fn init_gpu_resources(
114 &self,
115 device: &dyn GraphicsDevice,
116 pipeline_system: &dyn khora_core::renderer::traits::PipelineSystem,
117 ) -> Result<(), LaneError> {
118 let global_layout = pipeline_system
122 .inline_layout(device, UI_GLOBAL_LAYOUT_LABEL, &ui_global_layout_entries())
123 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))?;
124 let instance_layout = pipeline_system
125 .inline_layout(
126 device,
127 UI_INSTANCE_LAYOUT_LABEL,
128 &ui_instance_layout_entries(),
129 )
130 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))?;
131 let atlas_layout = pipeline_system
132 .inline_layout(device, UI_ATLAS_LAYOUT_LABEL, &ui_atlas_layout_entries())
133 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))?;
134
135 let pipeline_id = pipeline_system
138 .pipeline(device, &ui_pipeline_spec(device))
139 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))?;
140
141 let projection_buffer = device
143 .create_buffer(&BufferDescriptor {
144 label: Some(Cow::Borrowed("UI Projection Buffer")),
145 size: 64, usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
147 mapped_at_creation: false,
148 })
149 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))?;
150
151 let instance_buffer = device
152 .create_buffer(&BufferDescriptor {
153 label: Some(Cow::Borrowed("UI Instance Buffer")),
154 size: (self.max_instances * std::mem::size_of::<UiInstanceData>()) as u64,
155 usage: BufferUsage::STORAGE | BufferUsage::COPY_DST,
156 mapped_at_creation: false,
157 })
158 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))?;
159
160 let global_bind_group = device
162 .create_bind_group(&BindGroupDescriptor {
163 label: Some("ui_global_bind_group"),
164 layout: global_layout,
165 entries: &[BindGroupEntry {
166 binding: 0,
167 resource: BindingResource::Buffer(BufferBinding {
168 buffer: projection_buffer,
169 offset: 0,
170 size: None,
171 }),
172 _phantom: std::marker::PhantomData,
173 }],
174 })
175 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))?;
176
177 let instance_bind_group = device
178 .create_bind_group(&BindGroupDescriptor {
179 label: Some("ui_instance_bind_group"),
180 layout: instance_layout,
181 entries: &[BindGroupEntry {
182 binding: 0,
183 resource: BindingResource::Buffer(BufferBinding {
184 buffer: instance_buffer,
185 offset: 0,
186 size: None,
187 }),
188 _phantom: std::marker::PhantomData,
189 }],
190 })
191 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))?;
192
193 let _ = self.pipeline.set(pipeline_id);
196 let _ = self.global_layout.set(global_layout);
197 let _ = self.instance_layout.set(instance_layout);
198 let _ = self.projection_buffer.set(projection_buffer);
199 let _ = self.instance_buffer.set(instance_buffer);
200 let _ = self.global_bind_group.set(global_bind_group);
201 let _ = self.instance_bind_group.set(instance_bind_group);
202 let _ = self.atlas_layout.set(atlas_layout);
203
204 let sampler = device
205 .create_sampler(&khora_core::renderer::api::resource::SamplerDescriptor {
206 label: Some(Cow::Borrowed("ui_sampler")),
207 address_mode_u: khora_core::renderer::api::resource::AddressMode::ClampToEdge,
208 address_mode_v: khora_core::renderer::api::resource::AddressMode::ClampToEdge,
209 address_mode_w: khora_core::renderer::api::resource::AddressMode::ClampToEdge,
210 mag_filter: khora_core::renderer::api::resource::FilterMode::Linear,
211 min_filter: khora_core::renderer::api::resource::FilterMode::Linear,
212 mipmap_filter: khora_core::renderer::api::resource::MipmapFilterMode::Linear,
213 lod_min_clamp: 0.0,
214 lod_max_clamp: 100.0,
215 compare: None,
216 anisotropy_clamp: 1,
217 border_color: None,
218 })
219 .map_err(|e| LaneError::InitializationFailed(Box::new(e)))?;
220 let _ = self.sampler.set(sampler);
221
222 Ok(())
223 }
224}
225
226impl Lane for UiRenderLane {
227 fn strategy_name(&self) -> &'static str {
228 "UiRender"
229 }
230
231 fn lane_kind(&self) -> LaneKind {
232 LaneKind::Render
233 }
234
235 fn estimate_cost(&self, _ctx: &LaneContext) -> f32 {
236 0.1
237 }
238
239 fn on_initialize(&self, ctx: &mut LaneContext) -> Result<(), LaneError> {
240 let device = ctx
241 .get::<Arc<dyn GraphicsDevice>>()
242 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?
243 .clone();
244 let pipeline_system = ctx
245 .get::<Arc<dyn khora_core::renderer::traits::PipelineSystem>>()
246 .ok_or(LaneError::missing("Arc<dyn PipelineSystem>"))?
247 .clone();
248 self.init_gpu_resources(device.as_ref(), pipeline_system.as_ref())
249 }
250
251 fn execute(&self, ctx: &mut LaneContext) -> Result<(), LaneError> {
252 let device = ctx
253 .get::<Arc<dyn GraphicsDevice>>()
254 .ok_or(LaneError::missing("Arc<dyn GraphicsDevice>"))?;
255 let ui_scene = ctx
256 .get::<Ref<UiScene>>()
257 .ok_or(LaneError::missing("Ref<UiScene>"))?
258 .get();
259 let atlas_map = ctx
260 .get::<Ref<khora_data::ui::UiAtlasMap>>()
261 .ok_or(LaneError::missing("Ref<UiAtlasMap>"))?
262 .get();
263 let encoder = ctx
264 .get::<Slot<dyn khora_core::renderer::traits::CommandEncoder>>()
265 .ok_or(LaneError::missing("Slot<dyn CommandEncoder>"))?
266 .get();
267 let color_target = ctx
268 .get::<khora_core::lane::ColorTarget>()
269 .ok_or(LaneError::missing("ColorTarget"))?
270 .0;
271
272 let (width, height) = ui_scene.surface_size;
274 let projection = Mat4::orthographic_rh_zo(0.0, width as f32, height as f32, 0.0, 0.0, 1.0);
275
276 if let Some(buffer_id) = self.projection_buffer.get().copied() {
277 device
278 .write_buffer(buffer_id, 0, bytemuck::bytes_of(&projection))
279 .map_err(|e| LaneError::ExecutionFailed(Box::new(e)))?;
280 }
281
282 let mut instances = Vec::with_capacity(ui_scene.nodes.len().min(self.max_instances));
284
285 for node in &ui_scene.nodes {
286 if instances.len() >= self.max_instances {
287 break;
288 }
289
290 let color_vec = node.color.map(|c| c.0).unwrap_or(Vec4::ONE);
291 let border_radius = node.border.map(|b| b.radius).unwrap_or(0.0);
292 let border_width = node
293 .border
294 .map(|b| {
295 if b.width.left > 0.0 {
296 b.width.left
297 } else {
298 0.0
299 }
300 })
301 .unwrap_or(0.0);
302 let has_texture = if node.image.is_some() { 1.0 } else { 0.0 };
303
304 let (uv_min, uv_max) = node
305 .image
306 .and_then(|img| atlas_map.get(&img.texture))
307 .map(|rect| (rect.min.into(), rect.max.into()))
308 .unwrap_or(([0.0, 0.0], [1.0, 1.0]));
309
310 instances.push(UiInstanceData {
311 pos: node.pos.into(),
312 size: node.size.into(),
313 color: color_vec.into(),
314 params: [border_radius, border_width, has_texture, 0.0],
315 uv_min,
316 uv_max,
317 });
318 }
319
320 if instances.is_empty() {
321 return Ok(());
322 }
323
324 if let Some(buffer_id) = self.instance_buffer.get().copied() {
326 device
327 .write_buffer(buffer_id, 0, bytemuck::cast_slice(&instances))
328 .map_err(|e| LaneError::ExecutionFailed(Box::new(e)))?;
329 }
330
331 let mut atlas_bg = None;
338 if let Some(atlas_slot) = ctx.get::<Slot<khora_core::renderer::api::util::TextureAtlas>>() {
339 let atlas = atlas_slot.get();
340 if let (Some(layout), Some(sampler)) = (
341 self.atlas_layout.get().copied(),
342 self.sampler.get().copied(),
343 ) {
344 let view = atlas.view();
345 let mut cache = self
346 .atlas_bind_group
347 .lock()
348 .unwrap_or_else(|e| e.into_inner());
349 let bg = match *cache {
350 Some((cached_view, bg)) if cached_view == view => bg,
352 _ => {
355 let bg = device
356 .create_bind_group(&BindGroupDescriptor {
357 label: Some("ui_atlas_bind_group"),
358 layout,
359 entries: &[
360 BindGroupEntry {
361 binding: 0,
362 resource: BindingResource::TextureView(view),
363 _phantom: std::marker::PhantomData,
364 },
365 BindGroupEntry {
366 binding: 1,
367 resource: BindingResource::Sampler(sampler),
368 _phantom: std::marker::PhantomData,
369 },
370 ],
371 })
372 .map_err(|e| LaneError::ExecutionFailed(Box::new(e)))?;
373 if let Some((_, stale)) = cache.replace((view, bg)) {
374 let _ = device.destroy_bind_group(stale);
375 }
376 bg
377 }
378 };
379 atlas_bg = Some(bg);
380 }
381 }
382
383 if let (Some(pipeline_id), Some(g_bg), Some(i_bg)) = (
385 self.pipeline.get().copied(),
386 self.global_bind_group.get().copied(),
387 self.instance_bind_group.get().copied(),
388 ) {
389 let color_attachment = RenderPassColorAttachment {
390 view: &color_target,
391 resolve_target: None,
392 ops: Operations {
393 load: LoadOp::Load,
394 store: StoreOp::Store,
395 },
396 base_array_layer: 0,
397 base_mip_level: 0,
398 };
399
400 let attachments = [color_attachment];
401 let render_pass_desc = RenderPassDescriptor {
402 label: Some("UI Render Pass"),
403 color_attachments: &attachments,
404 depth_stencil_attachment: None,
405 };
406
407 let mut render_pass = encoder.begin_render_pass(&render_pass_desc);
408 render_pass.set_pipeline(&pipeline_id);
409 render_pass.set_bind_group(0, &g_bg, &[]);
410 render_pass.set_bind_group(1, &i_bg, &[]);
411
412 if let Some(bg) = &atlas_bg {
413 render_pass.set_bind_group(2, bg, &[]);
414 }
415
416 render_pass.draw(0..4, 0..(instances.len() as u32));
418 }
419
420 if let Some(tr) = ctx.get::<Arc<dyn TextRenderer>>() {
422 for text in &ui_scene.texts {
423 tr.queue_text(text.layout.as_ref(), text.pos, text.color, text.z_index);
424 }
425 tr.flush(device.as_ref(), encoder, &color_target)
426 .map_err(LaneError::ExecutionFailed)?;
427 }
428
429 Ok(())
430 }
431
432 fn as_any(&self) -> &dyn Any {
433 self
434 }
435
436 fn as_any_mut(&mut self) -> &mut dyn Any {
437 self
438 }
439}
440
441const UI_GLOBAL_LAYOUT_LABEL: &str = "ui_global_layout";
445const UI_INSTANCE_LAYOUT_LABEL: &str = "ui_instance_layout";
447const UI_ATLAS_LAYOUT_LABEL: &str = "ui_atlas_layout";
449
450fn ui_global_layout_entries() -> Vec<BindGroupLayoutEntry> {
452 vec![BindGroupLayoutEntry {
453 binding: 0,
454 visibility: ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
455 ty: BindingType::Buffer {
456 ty: BufferBindingType::Uniform,
457 has_dynamic_offset: false,
458 min_binding_size: None,
459 },
460 }]
461}
462
463fn ui_instance_layout_entries() -> Vec<BindGroupLayoutEntry> {
465 vec![BindGroupLayoutEntry {
466 binding: 0,
467 visibility: ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT,
468 ty: BindingType::Buffer {
469 ty: BufferBindingType::Storage { read_only: true },
470 has_dynamic_offset: false,
471 min_binding_size: None,
472 },
473 }]
474}
475
476fn ui_atlas_layout_entries() -> Vec<BindGroupLayoutEntry> {
478 vec![
479 BindGroupLayoutEntry {
480 binding: 0,
481 visibility: ShaderStageFlags::FRAGMENT,
482 ty: BindingType::Texture {
483 sample_type: khora_core::renderer::api::command::TextureSampleType::Float {
484 filterable: true,
485 },
486 view_dimension: TextureViewDimension::D2,
487 multisampled: false,
488 },
489 },
490 BindGroupLayoutEntry {
491 binding: 1,
492 visibility: ShaderStageFlags::FRAGMENT,
493 ty: BindingType::Sampler(
494 khora_core::renderer::api::command::SamplerBindingType::Filtering,
495 ),
496 },
497 ]
498}
499
500fn ui_pipeline_spec(
503 device: &dyn GraphicsDevice,
504) -> khora_core::renderer::api::pipeline::PipelineSpec {
505 use khora_core::renderer::api::pipeline::{LayoutSpec, PipelineSpec, ShaderVariantKey};
506 PipelineSpec {
507 label: "UI Render Pipeline",
508 shader: "khora::pipelines::ui",
509 variant: ShaderVariantKey::empty(),
510 bind_group_layouts: vec![
511 LayoutSpec::Inline {
512 label: UI_GLOBAL_LAYOUT_LABEL,
513 entries: Cow::Owned(ui_global_layout_entries()),
514 },
515 LayoutSpec::Inline {
516 label: UI_INSTANCE_LAYOUT_LABEL,
517 entries: Cow::Owned(ui_instance_layout_entries()),
518 },
519 LayoutSpec::Inline {
520 label: UI_ATLAS_LAYOUT_LABEL,
521 entries: Cow::Owned(ui_atlas_layout_entries()),
522 },
523 ],
524 vertex_buffers: vec![],
525 vs_entry: "vs_main",
526 fs_entry: Some("fs_main"),
527 primitive: PrimitiveStateDescriptor {
528 topology: PrimitiveTopology::TriangleList,
529 ..Default::default()
530 },
531 depth_stencil: None,
532 color_targets: vec![ColorTargetStateDescriptor {
533 format: device
534 .get_surface_format()
535 .unwrap_or(TextureFormat::Rgba8UnormSrgb),
536 blend: None,
537 write_mask: ColorWrites::ALL,
538 }],
539 multisample: MultisampleStateDescriptor {
540 count: SampleCount::X1,
541 mask: !0,
542 alpha_to_coverage_enabled: false,
543 },
544 }
545}