1use std::{any::Any, panic, sync::Arc};
2
3use crate::core::{Camera, PlotRenderConfig, PlotRenderer, RenderResult};
4use crate::gpu::util::map_read_async;
5use crate::plots::Figure;
6#[cfg(feature = "egui-overlay")]
7use crate::styling::ModernDarkTheme;
8use crate::styling::PlotThemeConfig;
9#[cfg(feature = "egui-overlay")]
10use runmat_time::Instant;
11
12#[cfg(feature = "egui-overlay")]
13use crate::overlay::plot_overlay::{OverlayConfig, OverlayMetrics, PlotOverlay};
14#[cfg(feature = "egui-overlay")]
15use egui_wgpu::ScreenDescriptor;
16
17pub const HEADLESS_GPU_ADAPTER_UNAVAILABLE: &str = "Failed to find suitable GPU adapter";
18pub const HEADLESS_GPU_DEVICE_CREATION_FAILED_PREFIX: &str = "Failed to create device:";
19pub const HEADLESS_GPU_CONTEXT_PANICKED_PREFIX: &str = "Headless GPU context creation panicked:";
20
21pub struct NativeSurfaceRenderContext {
23 renderer: PlotRenderer,
24 config: PlotRenderConfig,
25 pixels_per_point: f32,
26 background_policy: BackgroundPolicy,
27 textmark: Option<String>,
28 #[cfg(feature = "egui-overlay")]
29 overlay: Option<NativeOverlayState>,
30}
31
32#[derive(Debug, Clone, Copy)]
33enum BackgroundPolicy {
34 ThemeDriven,
35 Explicit(glam::Vec4),
36}
37
38#[cfg(feature = "egui-overlay")]
39struct NativeOverlayState {
40 egui_ctx: egui::Context,
41 egui_renderer: egui_wgpu::Renderer,
42 plot_overlay: PlotOverlay,
43}
44
45impl NativeSurfaceRenderContext {
46 pub async fn new(
48 device: Arc<wgpu::Device>,
49 queue: Arc<wgpu::Queue>,
50 width: u32,
51 height: u32,
52 format: wgpu::TextureFormat,
53 ) -> Result<Self, String> {
54 let surface_config = wgpu::SurfaceConfiguration {
55 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
56 format,
57 width: width.max(1),
58 height: height.max(1),
59 present_mode: wgpu::PresentMode::Fifo,
60 alpha_mode: wgpu::CompositeAlphaMode::Opaque,
61 view_formats: vec![],
62 desired_maximum_frame_latency: 1,
63 };
64 let renderer = PlotRenderer::new(device, queue, surface_config)
65 .await
66 .map_err(|err| format!("native surface renderer init failed: {err}"))?;
67 let config = PlotRenderConfig {
68 width: width.max(1),
69 height: height.max(1),
70 ..PlotRenderConfig::default()
71 };
72 #[cfg(feature = "egui-overlay")]
73 let overlay = {
74 let egui_ctx = egui::Context::default();
75 ModernDarkTheme::default().apply_to_egui(&egui_ctx);
76 let egui_renderer = crate::wgpu_compat::egui_renderer_new(
77 &renderer.wgpu_renderer.device,
78 format,
79 None,
80 1,
81 );
82 Some(NativeOverlayState {
83 egui_ctx,
84 egui_renderer,
85 plot_overlay: PlotOverlay::new(),
86 })
87 };
88
89 Ok(Self {
90 renderer,
91 config,
92 pixels_per_point: 1.0,
93 background_policy: BackgroundPolicy::ThemeDriven,
94 textmark: None,
95 #[cfg(feature = "egui-overlay")]
96 overlay,
97 })
98 }
99
100 pub fn resize(&mut self, width: u32, height: u32) {
102 let next_width = width.max(1);
103 let next_height = height.max(1);
104 self.config.width = next_width;
105 self.config.height = next_height;
106 self.renderer.wgpu_renderer.surface_config.width = next_width;
107 self.renderer.wgpu_renderer.surface_config.height = next_height;
108 self.renderer.on_surface_config_updated();
109 }
110
111 pub fn set_pixels_per_point(&mut self, pixels_per_point: f32) {
112 self.pixels_per_point = pixels_per_point.clamp(0.5, 4.0);
113 }
114
115 pub fn set_theme_config(&mut self, theme: PlotThemeConfig) {
116 self.renderer.theme = theme.clone();
117 self.config.theme = theme;
118 self.apply_background_policy();
119 }
120
121 pub fn set_textmark(&mut self, textmark: Option<&str>) {
122 self.textmark = textmark
123 .map(str::trim)
124 .filter(|s| !s.is_empty())
125 .map(ToOwned::to_owned);
126 }
127
128 pub fn render_to_view(
130 &mut self,
131 figure: &Figure,
132 view: &wgpu::TextureView,
133 camera: Option<&Camera>,
134 axes_cameras: Option<&[Camera]>,
135 ) -> Result<RenderResult, String> {
136 self.render_to_view_with_camera_state(figure, view, camera, axes_cameras, None)
137 }
138
139 pub fn render_to_view_with_camera_state(
140 &mut self,
141 figure: &Figure,
142 view: &wgpu::TextureView,
143 camera: Option<&Camera>,
144 axes_cameras: Option<&[Camera]>,
145 axes_camera_user_controlled: Option<&[bool]>,
146 ) -> Result<RenderResult, String> {
147 self.prepare_scene(figure, camera, axes_cameras, axes_camera_user_controlled);
148
149 let mut encoder = self.renderer.wgpu_renderer.device.create_command_encoder(
150 &wgpu::CommandEncoderDescriptor {
151 label: Some("Native Surface Render Encoder"),
152 },
153 );
154
155 let render_result = self.render_scene_with_overlay(&mut encoder, view)?;
156
157 self.renderer
158 .wgpu_renderer
159 .queue
160 .submit(std::iter::once(encoder.finish()));
161
162 Ok(render_result)
163 }
164
165 pub async fn render_to_rgba(
167 &mut self,
168 figure: &Figure,
169 camera: Option<&Camera>,
170 axes_cameras: Option<&[Camera]>,
171 ) -> Result<Vec<u8>, String> {
172 self.render_to_rgba_with_camera_state(figure, camera, axes_cameras, None)
173 .await
174 }
175
176 pub async fn render_to_rgba_with_camera_state(
177 &mut self,
178 figure: &Figure,
179 camera: Option<&Camera>,
180 axes_cameras: Option<&[Camera]>,
181 axes_camera_user_controlled: Option<&[bool]>,
182 ) -> Result<Vec<u8>, String> {
183 log::debug!(
184 "runmat-plot: native_surface.render_to_rgba.start width={} height={} axes={} overrides={}",
185 self.config.width.max(1),
186 self.config.height.max(1),
187 figure.axes_metadata.len(),
188 axes_cameras.map(|items| items.len()).unwrap_or(0)
189 );
190 self.prepare_scene(figure, camera, axes_cameras, axes_camera_user_controlled);
191
192 let width = self.config.width.max(1);
193 let height = self.config.height.max(1);
194 let format = self.renderer.wgpu_renderer.surface_config.format;
195 let device = self.renderer.wgpu_renderer.device.clone();
196 let queue = self.renderer.wgpu_renderer.queue.clone();
197
198 let color_texture = device.create_texture(&wgpu::TextureDescriptor {
199 label: Some("native_surface_offscreen_color"),
200 size: wgpu::Extent3d {
201 width,
202 height,
203 depth_or_array_layers: 1,
204 },
205 mip_level_count: 1,
206 sample_count: 1,
207 dimension: wgpu::TextureDimension::D2,
208 format,
209 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
210 view_formats: &[],
211 });
212 let color_view = color_texture.create_view(&wgpu::TextureViewDescriptor::default());
213
214 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
215 label: Some("Native Surface RGBA Render Encoder"),
216 });
217 log::debug!(
218 "runmat-plot: native_surface.render_to_rgba.render_scene width={} height={}",
219 width,
220 height
221 );
222 self.render_scene_with_overlay(&mut encoder, &color_view)?;
223 log::debug!("runmat-plot: native_surface.render_to_rgba.render_scene_ok");
224 queue.submit(std::iter::once(encoder.finish()));
225
226 let bytes_per_pixel = 4u32;
227 let padded_bytes_per_row = (width * bytes_per_pixel).div_ceil(256) * 256;
228 let output_buffer_size = (padded_bytes_per_row * height) as wgpu::BufferAddress;
229 let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
230 label: Some("native_surface_offscreen_readback"),
231 size: output_buffer_size,
232 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
233 mapped_at_creation: false,
234 });
235
236 let mut copy_encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
237 label: Some("Native Surface RGBA Copy Encoder"),
238 });
239 copy_encoder.copy_texture_to_buffer(
240 crate::wgpu_compat::TexelCopyTextureInfo {
241 texture: &color_texture,
242 mip_level: 0,
243 origin: wgpu::Origin3d::ZERO,
244 aspect: wgpu::TextureAspect::All,
245 },
246 crate::wgpu_compat::TexelCopyBufferInfo {
247 buffer: &output_buffer,
248 layout: crate::wgpu_compat::TexelCopyBufferLayout {
249 offset: 0,
250 bytes_per_row: Some(padded_bytes_per_row),
251 rows_per_image: Some(height),
252 },
253 },
254 wgpu::Extent3d {
255 width,
256 height,
257 depth_or_array_layers: 1,
258 },
259 );
260 queue.submit(std::iter::once(copy_encoder.finish()));
261 log::debug!(
262 "runmat-plot: native_surface.render_to_rgba.copy_submitted padded_bytes_per_row={} height={}",
263 padded_bytes_per_row,
264 height
265 );
266
267 let slice = output_buffer.slice(..);
268 map_read_async(device.as_ref(), &slice).await?;
269 log::debug!("runmat-plot: native_surface.render_to_rgba.readback_ready");
270 let data = slice.get_mapped_range();
271 let mut pixels = vec![0u8; (width * height * 4) as usize];
272 for row in 0..height as usize {
273 let src_start = row * padded_bytes_per_row as usize;
274 let dst_start = row * width as usize * 4;
275 pixels[dst_start..dst_start + width as usize * 4]
276 .copy_from_slice(&data[src_start..src_start + width as usize * 4]);
277 }
278 drop(data);
279 output_buffer.unmap();
280 log::debug!(
281 "runmat-plot: native_surface.render_to_rgba.ok bytes={}",
282 pixels.len()
283 );
284 Ok(pixels)
285 }
286
287 fn prepare_scene(
288 &mut self,
289 figure: &Figure,
290 camera: Option<&Camera>,
291 axes_cameras: Option<&[Camera]>,
292 axes_camera_user_controlled: Option<&[bool]>,
293 ) {
294 let bg = figure.background_color;
297 self.background_policy = if is_default_figure_bg(bg) {
298 BackgroundPolicy::ThemeDriven
299 } else {
300 BackgroundPolicy::Explicit(bg)
301 };
302 self.apply_background_policy();
303 self.config.show_grid = figure.grid_enabled
304 || figure.minor_grid_enabled
305 || figure
306 .axes_metadata
307 .iter()
308 .any(|meta| meta.grid_enabled || meta.minor_grid_enabled);
309 self.config.show_title = figure.has_any_titles();
310
311 self.renderer.set_figure(figure.clone());
312 if let Some(camera) = camera {
313 *self.renderer.camera_mut() = camera.clone();
314 }
315 if let Some(overrides) = axes_cameras {
316 for (index, override_camera) in overrides.iter().enumerate() {
317 if let Some(target) = self.renderer.axes_camera_mut(index) {
318 *target = override_camera.clone();
319 }
320 }
321 }
322 if let Some(flags) = axes_camera_user_controlled {
323 self.renderer.set_axes_camera_interaction_flags(flags);
324 }
325 }
326
327 fn render_scene_with_overlay(
328 &mut self,
329 encoder: &mut wgpu::CommandEncoder,
330 target_view: &wgpu::TextureView,
331 ) -> Result<RenderResult, String> {
332 #[cfg(feature = "egui-overlay")]
333 {
334 let Some(overlay) = self.overlay.as_mut() else {
335 log::debug!(
336 "runmat-plot: native_surface.render_scene_with_overlay.branch_no_overlay"
337 );
338 return self
339 .renderer
340 .render_scene_to_target(encoder, target_view, &self.config)
341 .map_err(|err| format!("native surface render failed: {err}"));
342 };
343
344 let start_time = Instant::now();
345 log::debug!(
346 "runmat-plot: native_surface.render_scene_with_overlay.start width={} height={} ppp={}",
347 self.config.width,
348 self.config.height,
349 self.pixels_per_point
350 );
351 let mut plot_area_points: Option<egui::Rect> = None;
352 let scene_stats = self.renderer.scene.statistics();
353 let _ = self.renderer.calculate_data_bounds();
354 let ppp = self.pixels_per_point.max(0.5);
355 let screen_rect = egui::Rect::from_min_size(
356 egui::Pos2::new(0.0, 0.0),
357 egui::Vec2::new(
358 (self.config.width.max(1) as f32) / ppp,
359 (self.config.height.max(1) as f32) / ppp,
360 ),
361 );
362 let full_output = overlay.egui_ctx.run(
363 egui::RawInput {
364 screen_rect: Some(screen_rect),
365 viewports: std::iter::once((
366 egui::ViewportId::ROOT,
367 egui::ViewportInfo {
368 native_pixels_per_point: Some(ppp),
369 inner_rect: Some(screen_rect),
370 outer_rect: Some(screen_rect),
371 focused: Some(true),
372 ..Default::default()
373 },
374 ))
375 .collect(),
376 ..Default::default()
377 },
378 |ctx| {
379 overlay
380 .plot_overlay
381 .set_theme_config(self.renderer.theme.clone());
382 overlay.plot_overlay.apply_theme(ctx);
383 let overlay_config = OverlayConfig {
384 show_grid: false,
386 show_toolbar: false,
388 font_scale: 1.25,
389 show_axes: true,
390 show_title: true,
391 title: self
392 .renderer
393 .overlay_title()
394 .cloned()
395 .or(Some("Plot".to_string())),
396 x_label: self
397 .renderer
398 .overlay_x_label()
399 .cloned()
400 .or(Some("X".to_string())),
401 y_label: self
402 .renderer
403 .overlay_y_label()
404 .cloned()
405 .or(Some("Y".to_string())),
406 show_sidebar: false,
407 ..Default::default()
408 };
409 let overlay_metrics = OverlayMetrics {
410 vertex_count: scene_stats.total_vertices,
411 triangle_count: scene_stats.total_triangles,
412 render_time_ms: 0.0,
413 fps: 60.0,
414 };
415 let frame_info = overlay.plot_overlay.render(
416 ctx,
417 &self.renderer,
418 &overlay_config,
419 overlay_metrics,
420 );
421 if let Some(textmark) = self.textmark.as_deref() {
422 let screen = ctx.screen_rect();
423 let anchor = egui::pos2(screen.max.x - 8.0, screen.max.y - 6.0);
424 let font =
425 egui::FontId::proportional(11.0 * overlay_config.font_scale.max(0.8));
426 let layer = egui::LayerId::new(
427 egui::Order::Foreground,
428 egui::Id::new("runmat_export_textmark"),
429 );
430 let painter = ctx.layer_painter(layer);
431 painter.text(
432 anchor + egui::vec2(1.0, 1.0),
433 egui::Align2::RIGHT_BOTTOM,
434 textmark,
435 font.clone(),
436 egui::Color32::from_rgba_premultiplied(0, 0, 0, 72),
437 );
438 painter.text(
439 anchor,
440 egui::Align2::RIGHT_BOTTOM,
441 textmark,
442 font,
443 egui::Color32::from_rgba_premultiplied(226, 234, 245, 96),
444 );
445 }
446 plot_area_points = frame_info.plot_area;
447 },
448 );
449
450 let paint_jobs = overlay
451 .egui_ctx
452 .tessellate(full_output.shapes, full_output.pixels_per_point);
453 for (id, image_delta) in &full_output.textures_delta.set {
454 overlay.egui_renderer.update_texture(
455 &self.renderer.wgpu_renderer.device,
456 &self.renderer.wgpu_renderer.queue,
457 *id,
458 image_delta,
459 );
460 }
461
462 let screen_descriptor = ScreenDescriptor {
463 size_in_pixels: [self.config.width.max(1), self.config.height.max(1)],
464 pixels_per_point: full_output.pixels_per_point,
465 };
466 overlay.egui_renderer.update_buffers(
467 &self.renderer.wgpu_renderer.device,
468 &self.renderer.wgpu_renderer.queue,
469 encoder,
470 &paint_jobs,
471 &screen_descriptor,
472 );
473
474 let (vx, vy, vw, vh) = if let Some(rect) = plot_area_points {
475 let vx = (rect.min.x * ppp).round().max(0.0) as u32;
476 let vy = (rect.min.y * ppp).round().max(0.0) as u32;
477 let vw = (rect.width() * ppp).round().max(1.0) as u32;
478 let vh = (rect.height() * ppp).round().max(1.0) as u32;
479 (vx, vy, vw, vh)
480 } else {
481 (0, 0, self.config.width.max(1), self.config.height.max(1))
482 };
483 let max_w = self.config.width.max(1);
484 let max_h = self.config.height.max(1);
485 let vx = vx.min(max_w.saturating_sub(1));
486 let vy = vy.min(max_h.saturating_sub(1));
487 let vw = vw.max(1).min(max_w.saturating_sub(vx).max(1));
488 let vh = vh.max(1).min(max_h.saturating_sub(vy).max(1));
489
490 if vw > 0 && vh > 0 {
491 self.renderer
492 .camera_mut()
493 .update_aspect_ratio((vw as f32) / (vh as f32));
494 }
495
496 let (rows, cols) = self.renderer.figure_axes_grid();
497 log::debug!(
498 "runmat-plot: native_surface.render_scene_with_overlay.axes_grid rows={} cols={} plot_area_present={}",
499 rows,
500 cols,
501 plot_area_points.is_some()
502 );
503 if rows * cols > 1 {
504 log::debug!(
505 "runmat-plot: native_surface.render_scene_with_overlay.branch_subplot_axes"
506 );
507 let rect_points = plot_area_points.unwrap_or_else(|| {
508 egui::Rect::from_min_size(
509 egui::Pos2::new(0.0, 0.0),
510 egui::Vec2::new(
511 (self.config.width.max(1) as f32) / ppp,
512 (self.config.height.max(1) as f32) / ppp,
513 ),
514 )
515 });
516 let existing_rect_count = overlay.plot_overlay.axes_plot_rects().len();
517 log::debug!(
518 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rect_source rows={} cols={} existing_rects={} expected_rects={}",
519 rows,
520 cols,
521 existing_rect_count,
522 rows * cols
523 );
524 let rects = if overlay.plot_overlay.axes_plot_rects().len() == rows * cols {
525 log::debug!(
526 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rect_source_existing rects={}",
527 existing_rect_count
528 );
529 overlay.plot_overlay.axes_plot_rects().to_vec()
530 } else {
531 log::debug!(
532 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rect_source_compute rect_points=({}, {})..({}, {})",
533 rect_points.min.x,
534 rect_points.min.y,
535 rect_points.max.x,
536 rect_points.max.y
537 );
538 overlay.plot_overlay.compute_subplot_plot_rects_snapped(
539 rect_points,
540 &self.renderer,
541 1.0,
542 ppp,
543 )
544 };
545 log::debug!(
546 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rects_ready rects={}",
547 rects.len()
548 );
549 let sw = self.config.width as f32;
550 let sh = self.config.height as f32;
551 let mut viewports: Vec<(u32, u32, u32, u32)> = Vec::with_capacity(rects.len());
552 for (rect_index, r) in rects.into_iter().enumerate() {
553 log::debug!(
554 "runmat-plot: native_surface.render_scene_with_overlay.subplot_rect viewport_index={} rect=({}, {})..({}, {})",
555 rect_index,
556 r.min.x,
557 r.min.y,
558 r.max.x,
559 r.max.y
560 );
561 let mut rx = (r.min.x * ppp).round().max(0.0);
562 let mut ry = (r.min.y * ppp).round().max(0.0);
563 let mut rw = (r.width() * ppp).round().max(1.0);
564 let mut rh = (r.height() * ppp).round().max(1.0);
565 if rx >= sw {
566 rx = (sw - 1.0).max(0.0);
567 }
568 if ry >= sh {
569 ry = (sh - 1.0).max(0.0);
570 }
571 if rx + rw > sw {
572 rw = (sw - rx).max(1.0);
573 }
574 if ry + rh > sh {
575 rh = (sh - ry).max(1.0);
576 }
577 viewports.push((rx as u32, ry as u32, rw as u32, rh as u32));
578 log::debug!(
579 "runmat-plot: native_surface.render_scene_with_overlay.subplot_viewport viewport_index={} viewport=({}, {}, {}, {})",
580 rect_index,
581 rx as u32,
582 ry as u32,
583 rw as u32,
584 rh as u32
585 );
586 }
587 log::debug!(
588 "runmat-plot: native_surface.render_scene_with_overlay.subplot_viewports_ready count={}",
589 viewports.len()
590 );
591 let axes_plot_sizes_px: Vec<(u32, u32)> = viewports
592 .iter()
593 .map(|&(_, _, w, h)| (w.max(1), h.max(1)))
594 .collect();
595 self.renderer
596 .ensure_scene_viewport_dependent_geometry_for_axes(&axes_plot_sizes_px);
597 self.renderer
598 .render_axes_to_viewports(
599 encoder,
600 target_view,
601 &viewports,
602 self.config.msaa_samples.max(1),
603 &self.config,
604 )
605 .map_err(|err| format!("native surface subplot render failed: {err}"))?;
606 log::debug!(
607 "runmat-plot: native_surface.render_scene_with_overlay.subplot_render_ok viewports={}",
608 viewports.len()
609 );
610 } else {
611 log::debug!(
612 "runmat-plot: native_surface.render_scene_with_overlay.branch_single_axes"
613 );
614 {
615 let clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
616 label: Some("runmat-native-single-axes-clear"),
617 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
618 view: target_view,
619 resolve_target: None,
620 ops: wgpu::Operations {
621 load: wgpu::LoadOp::Clear(wgpu::Color {
622 r: self.config.background_color.x as f64,
623 g: self.config.background_color.y as f64,
624 b: self.config.background_color.z as f64,
625 a: self.config.background_color.w as f64,
626 }),
627 store: wgpu::StoreOp::Store,
628 },
629 })],
630 depth_stencil_attachment: None,
631 timestamp_writes: None,
632 occlusion_query_set: None,
633 });
634 drop(clear_pass);
635 }
636
637 let mut cfg = self.config.clone();
638 cfg.width = vw.max(1);
639 cfg.height = vh.max(1);
640 let cam = self
641 .renderer
642 .axes_camera(0)
643 .cloned()
644 .unwrap_or_else(|| self.renderer.camera().clone());
645 let axes_plot_sizes_px = vec![(vw.max(1), vh.max(1))];
646 self.renderer
647 .ensure_scene_viewport_dependent_geometry_for_axes(&axes_plot_sizes_px);
648 self.renderer
649 .render_camera_to_viewport(
650 encoder,
651 target_view,
652 (vx, vy, vw, vh),
653 &cfg,
654 &cam,
655 0,
656 true,
657 )
658 .map_err(|err| format!("native surface viewport render failed: {err}"))?;
659 log::debug!(
660 "runmat-plot: native_surface.render_scene_with_overlay.single_axes_render_ok"
661 );
662 }
663
664 {
665 let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
666 label: Some("runmat-native-egui-overlay"),
667 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
668 view: target_view,
669 resolve_target: None,
670 ops: wgpu::Operations {
671 load: wgpu::LoadOp::Load,
672 store: wgpu::StoreOp::Store,
673 },
674 })],
675 depth_stencil_attachment: None,
676 timestamp_writes: None,
677 occlusion_query_set: None,
678 });
679 #[cfg(target_arch = "wasm32")]
680 let mut render_pass = render_pass.forget_lifetime();
681 #[cfg(not(target_arch = "wasm32"))]
682 let mut render_pass = render_pass;
683 overlay
684 .egui_renderer
685 .render(&mut render_pass, &paint_jobs, &screen_descriptor);
686 log::debug!(
687 "runmat-plot: native_surface.render_scene_with_overlay.overlay_ok paint_jobs={} textures_set={} textures_free={}",
688 paint_jobs.len(),
689 full_output.textures_delta.set.len(),
690 full_output.textures_delta.free.len()
691 );
692 }
693
694 for id in &full_output.textures_delta.free {
695 overlay.egui_renderer.free_texture(id);
696 }
697
698 Ok(RenderResult {
699 success: true,
700 data_bounds: self.renderer.data_bounds(),
701 vertex_count: scene_stats.total_vertices,
702 triangle_count: scene_stats.total_triangles,
703 render_time_ms: start_time.elapsed().as_secs_f64() * 1000.0,
704 })
705 }
706
707 #[cfg(not(feature = "egui-overlay"))]
708 {
709 self.renderer
710 .render_scene_to_target(encoder, target_view, &self.config)
711 .map_err(|err| format!("native surface render failed: {err}"))
712 }
713 }
714
715 fn apply_background_policy(&mut self) {
716 self.config.background_color = match self.background_policy {
717 BackgroundPolicy::ThemeDriven => {
718 self.renderer.theme.build_theme().get_background_color()
719 }
720 BackgroundPolicy::Explicit(color) => color,
721 };
722 }
723}
724
725fn is_default_figure_bg(bg: glam::Vec4) -> bool {
726 const EPS: f32 = 1e-3;
727 (bg.x - 1.0).abs() <= EPS
728 && (bg.y - 1.0).abs() <= EPS
729 && (bg.z - 1.0).abs() <= EPS
730 && (bg.w - 1.0).abs() <= EPS
731}
732
733async fn create_headless_context(
734 width: u32,
735 height: u32,
736) -> Result<NativeSurfaceRenderContext, String> {
737 let format = wgpu::TextureFormat::Rgba8Unorm;
741 if let Some(ctx) = crate::context::shared_wgpu_context() {
742 log::debug!(
743 "runmat-plot: native_surface.headless_context.branch_shared_context width={} height={}",
744 width,
745 height
746 );
747 return NativeSurfaceRenderContext::new(ctx.device, ctx.queue, width, height, format).await;
748 }
749
750 log::debug!(
751 "runmat-plot: native_surface.headless_context.branch_dedicated_context width={} height={}",
752 width,
753 height
754 );
755
756 let instance = panic::catch_unwind(|| {
757 crate::wgpu_compat::instance_new(wgpu::InstanceDescriptor::default())
758 })
759 .map_err(|payload| {
760 format!(
761 "{HEADLESS_GPU_CONTEXT_PANICKED_PREFIX} {}",
762 panic_payload_to_string(payload)
763 )
764 })?;
765 let adapter = instance
766 .request_adapter(&wgpu::RequestAdapterOptions {
767 power_preference: wgpu::PowerPreference::HighPerformance,
768 compatible_surface: None,
769 force_fallback_adapter: false,
770 })
771 .await
772 .ok_or(HEADLESS_GPU_ADAPTER_UNAVAILABLE)?;
773 let (device, queue) = adapter
774 .request_device(&crate::wgpu_compat::default_device_descriptor(), None)
775 .await
776 .map_err(|err| format!("{HEADLESS_GPU_DEVICE_CREATION_FAILED_PREFIX} {err}"))?;
777 let context =
778 NativeSurfaceRenderContext::new(Arc::new(device), Arc::new(queue), width, height, format)
779 .await?;
780 log::debug!(
781 "runmat-plot: native_surface.headless_context.ready width={} height={}",
782 width,
783 height
784 );
785 Ok(context)
786}
787
788pub fn is_headless_gpu_unavailable_error(err: &str) -> bool {
789 err.contains(HEADLESS_GPU_ADAPTER_UNAVAILABLE)
790 || err.contains(HEADLESS_GPU_DEVICE_CREATION_FAILED_PREFIX)
791 || err.contains(HEADLESS_GPU_CONTEXT_PANICKED_PREFIX)
792}
793
794fn panic_payload_to_string(payload: Box<dyn Any + Send>) -> String {
795 if let Some(message) = payload.downcast_ref::<&str>() {
796 (*message).to_string()
797 } else if let Some(message) = payload.downcast_ref::<String>() {
798 message.clone()
799 } else {
800 "unknown panic payload".to_string()
801 }
802}
803
804pub async fn render_figure_rgba_bytes_interactive_with_camera(
805 figure: Figure,
806 width: u32,
807 height: u32,
808 camera: &Camera,
809) -> Result<Vec<u8>, String> {
810 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
811 context.render_to_rgba(&figure, Some(camera), None).await
812}
813
814pub async fn render_figure_rgba_bytes_interactive_with_camera_and_theme(
815 figure: Figure,
816 width: u32,
817 height: u32,
818 camera: &Camera,
819 theme: PlotThemeConfig,
820) -> Result<Vec<u8>, String> {
821 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
822 context.set_theme_config(theme);
823 context.render_to_rgba(&figure, Some(camera), None).await
824}
825
826pub async fn render_figure_rgba_bytes_interactive_with_axes_cameras(
827 figure: Figure,
828 width: u32,
829 height: u32,
830 axes_cameras: &[Camera],
831) -> Result<Vec<u8>, String> {
832 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
833 context
834 .render_to_rgba(&figure, None, Some(axes_cameras))
835 .await
836}
837
838pub async fn render_figure_rgba_bytes_interactive_with_axes_cameras_and_theme(
839 figure: Figure,
840 width: u32,
841 height: u32,
842 axes_cameras: &[Camera],
843 theme: PlotThemeConfig,
844) -> Result<Vec<u8>, String> {
845 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
846 context.set_theme_config(theme);
847 context
848 .render_to_rgba(&figure, None, Some(axes_cameras))
849 .await
850}
851
852pub async fn render_figure_rgba_bytes_interactive(
853 figure: Figure,
854 width: u32,
855 height: u32,
856) -> Result<Vec<u8>, String> {
857 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
858 context.render_to_rgba(&figure, None, None).await
859}
860
861pub async fn render_figure_rgba_bytes_interactive_and_theme(
862 figure: Figure,
863 width: u32,
864 height: u32,
865 theme: PlotThemeConfig,
866) -> Result<Vec<u8>, String> {
867 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
868 context.set_theme_config(theme);
869 context.render_to_rgba(&figure, None, None).await
870}
871
872fn encode_png_bytes(width: u32, height: u32, rgba: &[u8]) -> Result<Vec<u8>, String> {
873 use image::{ImageBuffer, ImageFormat, Rgba};
874
875 let mut opaque = rgba.to_vec();
876 for pixel in opaque.chunks_exact_mut(4) {
877 pixel[3] = 255;
878 }
879
880 let image = ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, opaque)
881 .ok_or_else(|| "Failed to create image buffer for PNG encoding".to_string())?;
882 let mut out = std::io::Cursor::new(Vec::new());
883 image
884 .write_to(&mut out, ImageFormat::Png)
885 .map_err(|err| format!("Failed to encode PNG bytes: {err}"))?;
886 Ok(out.into_inner())
887}
888
889pub async fn render_figure_png_bytes_interactive(
890 figure: Figure,
891 width: u32,
892 height: u32,
893) -> Result<Vec<u8>, String> {
894 let rgba = render_figure_rgba_bytes_interactive(figure, width, height).await?;
895 encode_png_bytes(width.max(1), height.max(1), &rgba)
896}
897
898pub async fn render_figure_png_bytes_interactive_and_theme(
899 figure: Figure,
900 width: u32,
901 height: u32,
902 theme: PlotThemeConfig,
903) -> Result<Vec<u8>, String> {
904 let rgba = render_figure_rgba_bytes_interactive_and_theme(figure, width, height, theme).await?;
905 encode_png_bytes(width.max(1), height.max(1), &rgba)
906}
907
908pub async fn render_figure_png_bytes_interactive_and_theme_and_textmark(
909 figure: Figure,
910 width: u32,
911 height: u32,
912 theme: PlotThemeConfig,
913 textmark: Option<&str>,
914) -> Result<Vec<u8>, String> {
915 log::debug!(
916 "runmat-plot: render_figure_png_bytes_interactive_and_theme_and_textmark.start width={} height={} axes={} textmark={}",
917 width,
918 height,
919 figure.axes_metadata.len(),
920 textmark.unwrap_or("")
921 );
922 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
923 log::debug!(
924 "runmat-plot: render_figure_png_bytes_interactive_and_theme_and_textmark.context_ready width={} height={}",
925 width.max(1),
926 height.max(1)
927 );
928 context.set_theme_config(theme);
929 context.set_textmark(textmark);
930 let rgba = context.render_to_rgba(&figure, None, None).await?;
931 log::debug!(
932 "runmat-plot: render_figure_png_bytes_interactive_and_theme_and_textmark.rgba_ready bytes={}",
933 rgba.len()
934 );
935 encode_png_bytes(width.max(1), height.max(1), &rgba)
936}
937
938pub async fn render_figure_png_bytes_interactive_with_camera(
939 figure: Figure,
940 width: u32,
941 height: u32,
942 camera: &Camera,
943) -> Result<Vec<u8>, String> {
944 let rgba =
945 render_figure_rgba_bytes_interactive_with_camera(figure, width, height, camera).await?;
946 encode_png_bytes(width.max(1), height.max(1), &rgba)
947}
948
949pub async fn render_figure_png_bytes_interactive_with_camera_and_theme(
950 figure: Figure,
951 width: u32,
952 height: u32,
953 camera: &Camera,
954 theme: PlotThemeConfig,
955) -> Result<Vec<u8>, String> {
956 let rgba = render_figure_rgba_bytes_interactive_with_camera_and_theme(
957 figure, width, height, camera, theme,
958 )
959 .await?;
960 encode_png_bytes(width.max(1), height.max(1), &rgba)
961}
962
963pub async fn render_figure_png_bytes_interactive_with_camera_and_theme_and_textmark(
964 figure: Figure,
965 width: u32,
966 height: u32,
967 camera: &Camera,
968 theme: PlotThemeConfig,
969 textmark: Option<&str>,
970) -> Result<Vec<u8>, String> {
971 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
972 context.set_theme_config(theme);
973 context.set_textmark(textmark);
974 let rgba = context.render_to_rgba(&figure, Some(camera), None).await?;
975 encode_png_bytes(width.max(1), height.max(1), &rgba)
976}
977
978pub async fn render_figure_png_bytes_interactive_with_axes_cameras(
979 figure: Figure,
980 width: u32,
981 height: u32,
982 axes_cameras: &[Camera],
983) -> Result<Vec<u8>, String> {
984 let rgba =
985 render_figure_rgba_bytes_interactive_with_axes_cameras(figure, width, height, axes_cameras)
986 .await?;
987 encode_png_bytes(width.max(1), height.max(1), &rgba)
988}
989
990pub async fn render_figure_png_bytes_interactive_with_axes_cameras_and_theme(
991 figure: Figure,
992 width: u32,
993 height: u32,
994 axes_cameras: &[Camera],
995 theme: PlotThemeConfig,
996) -> Result<Vec<u8>, String> {
997 let rgba = render_figure_rgba_bytes_interactive_with_axes_cameras_and_theme(
998 figure,
999 width,
1000 height,
1001 axes_cameras,
1002 theme,
1003 )
1004 .await?;
1005 encode_png_bytes(width.max(1), height.max(1), &rgba)
1006}
1007
1008pub async fn render_figure_png_bytes_interactive_with_axes_cameras_and_theme_and_textmark(
1009 figure: Figure,
1010 width: u32,
1011 height: u32,
1012 axes_cameras: &[Camera],
1013 theme: PlotThemeConfig,
1014 textmark: Option<&str>,
1015) -> Result<Vec<u8>, String> {
1016 log::debug!(
1017 "runmat-plot: render_figure_png_bytes_interactive_with_axes_cameras_and_theme_and_textmark.start width={} height={} axes={} camera_overrides={} textmark={}",
1018 width,
1019 height,
1020 figure.axes_metadata.len(),
1021 axes_cameras.len(),
1022 textmark.unwrap_or("")
1023 );
1024 let mut context = create_headless_context(width.max(1), height.max(1)).await?;
1025 log::debug!(
1026 "runmat-plot: render_figure_png_bytes_interactive_with_axes_cameras_and_theme_and_textmark.context_ready width={} height={}",
1027 width.max(1),
1028 height.max(1)
1029 );
1030 context.set_theme_config(theme);
1031 context.set_textmark(textmark);
1032 let rgba = context
1033 .render_to_rgba(&figure, None, Some(axes_cameras))
1034 .await?;
1035 log::debug!(
1036 "runmat-plot: render_figure_png_bytes_interactive_with_axes_cameras_and_theme_and_textmark.rgba_ready bytes={}",
1037 rgba.len()
1038 );
1039 encode_png_bytes(width.max(1), height.max(1), &rgba)
1040}
1041
1042#[cfg(test)]
1043mod tests {
1044 use super::*;
1045
1046 #[test]
1047 fn headless_gpu_panic_errors_are_fallback_eligible() {
1048 assert!(is_headless_gpu_unavailable_error(
1049 "Headless GPU context creation panicked: called `Option::unwrap()` on a `None` value"
1050 ));
1051 }
1052}