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