use std::collections::HashMap;
use std::time::{Duration, Instant};
use wgpu::*;
#[derive(Debug, thiserror::Error)]
pub enum RenderError {
#[error("WebGPU error: {0}")]
WebGPU(#[from] wgpu::Error),
#[error("WebGL error: {0}")]
WebGL(String),
#[error("Canvas error: {0}")]
Canvas(String),
#[error("Buffer error: {0}")]
Buffer(String),
#[error("Shader error: {0}")]
Shader(String),
#[error("Performance error: {0}")]
Performance(String),
}
#[derive(Debug, Clone)]
pub enum RenderBackend {
WebGPU {
device: Option<Device>,
queue: Option<Queue>,
surface: Option<Surface>,
compute_capability: bool,
memory_budget: usize,
},
WebGL2 {
context: Option<String>, extensions: Vec<String>,
},
Canvas2D { context: Option<String> }, }
impl RenderBackend {
pub async fn create_optimal() -> Result<Self, RenderError> {
if Self::webgpu_available().await {
Self::webgpu_backend().await
} else if Self::webgl2_available() {
Self::webgl2_backend()
} else {
Self::canvas2d_backend()
}
}
async fn webgpu_available() -> bool {
#[cfg(target_arch = "wasm32")]
{
false }
#[cfg(not(target_arch = "wasm32"))]
{
false }
}
fn webgl2_available() -> bool {
#[cfg(target_arch = "wasm32")]
{
false }
#[cfg(not(target_arch = "wasm32"))]
{
false }
}
async fn webgpu_backend() -> Result<Self, RenderError> {
Err(RenderError::WebGPU(wgpu::Error::DeviceLost))
}
fn webgl2_backend() -> Result<Self, RenderError> {
Err(RenderError::WebGL("WebGL2 not implemented".to_string()))
}
fn canvas2d_backend() -> Result<Self, RenderError> {
Err(RenderError::Canvas("Canvas 2D not implemented".to_string()))
}
pub fn performance_characteristics(&self) -> PerformanceProfile {
match self {
RenderBackend::WebGPU { .. } => PerformanceProfile {
max_points: 10_000_000,
target_fps: 60,
memory_efficiency: 0.95,
compute_shaders: true,
},
RenderBackend::WebGL2 { .. } => PerformanceProfile {
max_points: 1_000_000,
target_fps: 60,
memory_efficiency: 0.80,
compute_shaders: false,
},
RenderBackend::Canvas2D { .. } => PerformanceProfile {
max_points: 10_000,
target_fps: 30,
memory_efficiency: 0.60,
compute_shaders: false,
},
}
}
}
#[derive(Debug, Clone)]
pub struct PerformanceProfile {
pub max_points: u32,
pub target_fps: u32,
pub memory_efficiency: f32,
pub compute_shaders: bool,
}
#[derive(Clone)]
pub struct Renderer {
backend: RenderBackend,
pipelines: HashMap<ChartType, RenderPipeline>,
buffer_pool: BufferPool,
frame_timer: FrameTimer,
quality_manager: AdaptiveQualityManager,
}
impl Renderer {
pub async fn new() -> Result<Self, RenderError> {
let backend = RenderBackend::create_optimal().await?;
let buffer_pool = BufferPool::new(&backend)?;
let frame_timer = FrameTimer::new();
let quality_manager = AdaptiveQualityManager::new();
Ok(Self {
backend,
pipelines: HashMap::new(),
buffer_pool,
frame_timer,
quality_manager,
})
}
pub fn render(&mut self, spec: &ChartSpec) -> RenderStats {
let start_time = Instant::now();
let quality_level = self.frame_timer.suggest_quality();
let render_config = self.quality_manager.get_render_config(quality_level);
let chart_type = ChartType::from_spec(spec);
let pipeline = self.get_or_create_pipeline(chart_type);
let buffers = self.buffer_pool.get_buffers_for_spec(spec);
let stats = self.execute_render_pass(&pipeline, &buffers, &render_config);
let frame_time = start_time.elapsed();
self.frame_timer.record_frame(frame_time);
self.quality_manager.update_frame_stats(frame_time);
stats
}
fn get_or_create_pipeline(&mut self, chart_type: ChartType) -> &RenderPipeline {
self.pipelines
.entry(chart_type)
.or_insert_with(|| RenderPipeline::new(&self.backend, chart_type))
}
fn execute_render_pass(
&self,
pipeline: &RenderPipeline,
buffers: &RenderBuffers,
config: &RenderConfig,
) -> RenderStats {
RenderStats {
frame_time: Duration::from_millis(3), triangles_rendered: 1000, draw_calls: 1, memory_used: 1024 * 1024, gpu_utilization: 0.5, cache_hit_rate: 0.95, }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ChartType {
Point,
Line,
Bar,
Area,
Text,
Rect,
Composite,
}
impl ChartType {
pub fn from_spec(spec: &ChartSpec) -> Self {
match spec.mark {
MarkType::Point { .. } => ChartType::Point,
MarkType::Line { .. } => ChartType::Line,
MarkType::Bar { .. } => ChartType::Bar,
MarkType::Area { .. } => ChartType::Area,
MarkType::Text { .. } => ChartType::Text,
MarkType::Rect { .. } => ChartType::Rect,
MarkType::Composite(_) => ChartType::Composite,
}
}
}
pub struct RenderPipeline {
chart_type: ChartType,
vertex_shader: ShaderModule,
fragment_shader: ShaderModule,
compute_shader: Option<ShaderModule>,
render_pipeline: wgpu::RenderPipeline,
compute_pipeline: Option<wgpu::ComputePipeline>,
}
impl RenderPipeline {
pub fn new(backend: &RenderBackend, chart_type: ChartType) -> Self {
match backend {
RenderBackend::WebGPU { device, .. } => {
Self::create_webgpu_pipeline(device, chart_type)
}
_ => {
Self {
chart_type,
vertex_shader: ShaderModule::default(),
fragment_shader: ShaderModule::default(),
compute_shader: None,
render_pipeline: wgpu::RenderPipeline::default(),
compute_pipeline: None,
}
}
}
}
fn create_webgpu_pipeline(device: &Device, chart_type: ChartType) -> Self {
let vertex_shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("Vertex Shader"),
source: ShaderSource::Wgsl(include_str!("shaders/vertex.wgsl").into()),
});
let fragment_shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("Fragment Shader"),
source: ShaderSource::Wgsl(include_str!("shaders/fragment.wgsl").into()),
});
let render_pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
label: Some("Render Pipeline"),
layout: None,
vertex: VertexState {
module: &vertex_shader,
entry_point: "vs_main",
buffers: &[],
},
fragment: Some(FragmentState {
module: &fragment_shader,
entry_point: "fs_main",
targets: &[Some(ColorTargetState {
format: TextureFormat::Bgra8UnormSrgb,
blend: Some(BlendState::ALPHA_BLENDING),
write_mask: ColorWrites::ALL,
})],
}),
primitive: PrimitiveState {
topology: PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: FrontFace::Ccw,
cull_mode: Some(Face::Back),
polygon_mode: PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
multiview: None,
});
Self {
chart_type,
vertex_shader,
fragment_shader,
compute_shader: None,
render_pipeline,
compute_pipeline: None,
}
}
}
pub struct BufferPool {
device: Arc<Device>,
available_buffers: HashMap<BufferSpec, VecDeque<Buffer>>,
allocated_size: AtomicUsize,
max_pool_size: usize,
}
impl BufferPool {
pub fn new(backend: &RenderBackend) -> Result<Self, RenderError> {
match backend {
RenderBackend::WebGPU { device, .. } => {
Ok(Self {
device: Arc::new(device.clone()),
available_buffers: HashMap::new(),
allocated_size: AtomicUsize::new(0),
max_pool_size: 100 * 1024 * 1024, })
}
_ => Err(RenderError::Buffer(
"Buffer pool requires WebGPU backend".to_string(),
)),
}
}
pub fn get_buffer(&mut self, spec: BufferSpec) -> Buffer {
if let Some(buffer) = self
.available_buffers
.get_mut(&spec)
.and_then(|queue| queue.pop_front())
{
return buffer;
}
self.create_buffer(spec)
}
pub fn return_buffer(&mut self, buffer: Buffer, spec: BufferSpec) {
if self.allocated_size.load(Ordering::Relaxed) < self.max_pool_size {
self.available_buffers
.entry(spec)
.or_default()
.push_back(buffer);
}
}
fn create_buffer(&self, spec: BufferSpec) -> Buffer {
self.allocated_size.fetch_add(spec.size, Ordering::Relaxed);
self.device.create_buffer(&BufferDescriptor {
label: Some(&spec.label),
size: spec.size as u64,
usage: spec.usage,
mapped_at_creation: false,
})
}
pub fn get_buffers_for_spec(&mut self, spec: &ChartSpec) -> RenderBuffers {
RenderBuffers {
vertex_buffer: self.get_buffer(BufferSpec {
label: "vertex".to_string(),
size: 1024 * 1024, usage: BufferUsages::VERTEX,
}),
index_buffer: self.get_buffer(BufferSpec {
label: "index".to_string(),
size: 256 * 1024, usage: BufferUsages::INDEX,
}),
uniform_buffer: self.get_buffer(BufferSpec {
label: "uniform".to_string(),
size: 64 * 1024, usage: BufferUsages::UNIFORM,
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BufferSpec {
pub label: String,
pub size: usize,
pub usage: BufferUsages,
}
pub struct RenderBuffers {
pub vertex_buffer: Buffer,
pub index_buffer: Buffer,
pub uniform_buffer: Buffer,
}
pub struct FrameTimer {
frame_times: VecDeque<Duration>,
max_samples: usize,
}
impl FrameTimer {
pub fn new() -> Self {
Self {
frame_times: VecDeque::new(),
max_samples: 60, }
}
pub fn record_frame(&mut self, frame_time: Duration) {
self.frame_times.push_back(frame_time);
if self.frame_times.len() > self.max_samples {
self.frame_times.pop_front();
}
}
pub fn average_frame_time(&self) -> Duration {
if self.frame_times.is_empty() {
return Duration::from_millis(16); }
let total: Duration = self.frame_times.iter().sum();
total / self.frame_times.len() as u32
}
pub fn fps(&self) -> f64 {
let avg_frame_time = self.average_frame_time();
if avg_frame_time.as_secs_f64() > 0.0 {
1.0 / avg_frame_time.as_secs_f64()
} else {
0.0
}
}
pub fn suggest_quality(&self) -> f32 {
let avg_frame_time = self.average_frame_time();
let target_frame_time = Duration::from_millis(16);
if avg_frame_time > target_frame_time * 2 {
0.3 } else if avg_frame_time > target_frame_time * 1.5 {
0.5 } else if avg_frame_time < target_frame_time * 0.5 {
1.0 } else {
0.8 }
}
}
pub struct AdaptiveQualityManager {
frame_timer: FrameTimer,
quality_level: f32,
target_frame_time: Duration,
quality_config: QualityConfig,
}
impl AdaptiveQualityManager {
pub fn new() -> Self {
Self {
frame_timer: FrameTimer::new(),
quality_level: 0.8,
target_frame_time: Duration::from_millis(16), quality_config: QualityConfig::default(),
}
}
pub fn update_frame_stats(&mut self, frame_time: Duration) {
self.frame_timer.record_frame(frame_time);
let avg_frame_time = self.frame_timer.average_frame_time();
if avg_frame_time > self.target_frame_time * 1.2 {
self.quality_level = (self.quality_level - 0.1).max(0.3);
} else if avg_frame_time < self.target_frame_time * 0.8 {
self.quality_level = (self.quality_level + 0.05).min(1.0);
}
}
pub fn get_render_config(&self, quality_level: f32) -> RenderConfig {
RenderConfig {
point_size: self.quality_config.base_point_size * quality_level,
anti_aliasing: quality_level > 0.7,
msaa_samples: if quality_level > 0.8 { 4 } else { 1 },
lod_bias: (1.0 - quality_level) * 2.0,
texture_filtering: if quality_level > 0.6 {
FilterMode::Linear
} else {
FilterMode::Nearest
},
}
}
}
#[derive(Debug, Clone)]
pub struct QualityConfig {
pub base_point_size: f32,
}
impl Default for QualityConfig {
fn default() -> Self {
Self {
base_point_size: 4.0,
}
}
}
#[derive(Debug, Clone)]
pub struct RenderConfig {
pub point_size: f32,
pub anti_aliasing: bool,
pub msaa_samples: u32,
pub lod_bias: f32,
pub texture_filtering: FilterMode,
}
#[derive(Debug, Clone)]
pub enum FilterMode {
Nearest,
Linear,
}
#[derive(Debug, Clone)]
pub struct RenderStats {
pub frame_time: Duration,
pub triangles_rendered: u32,
pub draw_calls: u32,
pub memory_used: usize,
pub gpu_utilization: f32,
pub cache_hit_rate: f32,
}
impl RenderStats {
pub fn fps(&self) -> f64 {
if self.frame_time.as_secs_f64() > 0.0 {
1.0 / self.frame_time.as_secs_f64()
} else {
0.0
}
}
pub fn is_within_budget(&self, budget: &PerformanceBudget) -> bool {
self.frame_time <= budget.max_frame_time
&& self.memory_used <= budget.max_memory
&& self.gpu_utilization <= budget.max_gpu_utilization
}
pub fn suggest_optimizations(&self) -> Vec<OptimizationSuggestion> {
let mut suggestions = Vec::new();
if self.frame_time > Duration::from_millis(16) {
suggestions.push(OptimizationSuggestion::ReduceQuality);
}
if self.memory_used > 100 * 1024 * 1024 {
suggestions.push(OptimizationSuggestion::ReduceDataSize);
}
if self.gpu_utilization > 0.9 {
suggestions.push(OptimizationSuggestion::EnableLOD);
}
if self.cache_hit_rate < 0.8 {
suggestions.push(OptimizationSuggestion::ImproveCaching);
}
suggestions
}
}
#[derive(Debug, Clone)]
pub struct PerformanceBudget {
pub max_frame_time: Duration,
pub max_memory: usize,
pub max_gpu_utilization: f32,
}
#[derive(Debug, Clone)]
pub enum OptimizationSuggestion {
ReduceQuality,
ReduceDataSize,
EnableLOD,
ImproveCaching,
UseStreaming,
EnableGPUProcessing,
}
use crate::chart::{ChartSpec, MarkType};
use std::collections::VecDeque;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ChartType {
Line,
Bar,
Scatter,
Area,
}
impl ChartType {
pub fn from_spec(spec: &ChartSpec) -> Self {
match spec.mark {
MarkType::Line { .. } => ChartType::Line,
MarkType::Bar { .. } => ChartType::Bar,
MarkType::Point { .. } => ChartType::Scatter,
MarkType::Area { .. } => ChartType::Area,
}
}
}
#[derive(Debug, Clone)]
pub struct RenderPipeline {
pub chart_type: ChartType,
pub optimized: bool,
}
impl RenderPipeline {
pub fn new(backend: &RenderBackend, chart_type: ChartType) -> Self {
Self {
chart_type,
optimized: matches!(backend, RenderBackend::WebGPU { .. }),
}
}
pub fn is_optimized(&self) -> bool {
self.optimized
}
}
#[derive(Debug, Clone)]
pub struct BufferPool {
pub available_buffers: Vec<Buffer>,
pub used_buffers: Vec<Buffer>,
}
impl BufferPool {
pub fn new(backend: &RenderBackend) -> Result<Self, RenderError> {
Ok(Self {
available_buffers: Vec::new(),
used_buffers: Vec::new(),
})
}
pub fn get_buffers_for_spec(&self, spec: &ChartSpec) -> RenderBuffers {
RenderBuffers {
vertex_buffer: None,
index_buffer: None,
uniform_buffer: None,
}
}
pub fn allocate_buffer(&mut self, size: usize) -> Result<Buffer, RenderError> {
let buffer = Buffer {
size,
data: vec![0u8; size],
};
self.used_buffers.push(buffer.clone());
Ok(buffer)
}
pub fn return_buffer(&mut self, buffer: Buffer) {
if let Some(pos) = self.used_buffers.iter().position(|b| b.size == buffer.size) {
self.used_buffers.remove(pos);
self.available_buffers.push(buffer);
}
}
pub fn get_stats(&self) -> BufferPoolStats {
BufferPoolStats {
total_allocations: self.used_buffers.len() + self.available_buffers.len(),
total_deallocations: 0,
current_allocations: self.used_buffers.len(),
available_buffers: self.available_buffers.len(),
reuse_count: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct Buffer {
pub size: usize,
pub data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct RenderBuffers {
pub vertex_buffer: Option<Buffer>,
pub index_buffer: Option<Buffer>,
pub uniform_buffer: Option<Buffer>,
}
#[derive(Debug, Clone)]
pub struct BufferPoolStats {
pub total_allocations: usize,
pub total_deallocations: usize,
pub current_allocations: usize,
pub available_buffers: usize,
pub reuse_count: usize,
}
#[derive(Debug, Clone)]
pub struct FrameTimer {
pub frame_times: VecDeque<Duration>,
pub max_samples: usize,
}
impl FrameTimer {
pub fn new() -> Self {
Self {
frame_times: VecDeque::new(),
max_samples: 60, }
}
pub fn record_frame(&mut self, frame_time: Duration) {
self.frame_times.push_back(frame_time);
if self.frame_times.len() > self.max_samples {
self.frame_times.pop_front();
}
}
pub fn suggest_quality(&self) -> f32 {
if self.frame_times.is_empty() {
return 1.0;
}
let avg_frame_time = self.frame_times.iter()
.map(|d| d.as_secs_f32())
.sum::<f32>() / self.frame_times.len() as f32;
if avg_frame_time > 0.016 { 0.5 } else {
1.0 }
}
}
#[derive(Debug, Clone)]
pub struct AdaptiveQualityManager {
pub quality_level: f32,
}
impl AdaptiveQualityManager {
pub fn new() -> Self {
Self {
quality_level: 1.0,
}
}
pub fn get_render_config(&self, quality: f32) -> RenderConfig {
RenderConfig {
quality_level: quality,
enable_lod: quality < 0.8,
reduce_shadows: quality < 0.6,
}
}
pub fn update_frame_stats(&mut self, frame_time: Duration) {
if frame_time > Duration::from_millis(16) {
self.quality_level = (self.quality_level * 0.9).max(0.1);
} else {
self.quality_level = (self.quality_level * 1.1).min(1.0);
}
}
pub fn set_quality_level(&mut self, level: f32) {
self.quality_level = level.clamp(0.0, 1.0);
}
}
#[derive(Debug, Clone)]
pub struct RenderConfig {
pub quality_level: f32,
pub enable_lod: bool,
pub reduce_shadows: bool,
}