437 lines
14 KiB
Rust
437 lines
14 KiB
Rust
// wgpu Integration - GPU-Accelerated UI Rendering for egui
|
|
// Production-ready GPU-accelerated rendering patterns
|
|
//
|
|
// Last Updated: 2026-03-14
|
|
// egui Version: 0.27+
|
|
// Rust Version: 1.85+
|
|
// wgpu Version: 0.20+
|
|
|
|
use egui::{Context, ClippedPrimitive, TexturesDelta};
|
|
use wgpu::{
|
|
Device, Queue, Buffer, RenderPipeline, RenderPassDescriptor,
|
|
Texture, TextureView, BufferInitDescriptor, BufferUsages,
|
|
CommandEncoder, TextureAspect, ImageInfo,
|
|
};
|
|
use std::sync::Arc;
|
|
|
|
/// WgpuRenderer - GPU-accelerated egui rendering
|
|
pub struct WgpuRenderer {
|
|
/// wgpu device
|
|
pub device: Arc<Device>,
|
|
/// wgpu queue
|
|
pub queue: Arc<Queue>,
|
|
/// egui render pipeline
|
|
pub pipeline: RenderPipeline,
|
|
/// egui vertex buffer
|
|
pub vertex_buffer: Buffer,
|
|
/// egui texture
|
|
pub texture: Texture,
|
|
/// Texture view
|
|
pub texture_view: TextureView,
|
|
/// Current sequence
|
|
pub sequence: u64,
|
|
/// GPU memory usage
|
|
pub gpu_memory_bytes: u64,
|
|
/// Validation enabled
|
|
pub validation_enabled: bool,
|
|
}
|
|
|
|
/// Implementation: WgpuRenderer
|
|
impl WgpuRenderer {
|
|
/// Create new wgpu renderer
|
|
pub fn new(device: Device, queue: Queue, ctx: Context) -> Self {
|
|
let pipeline = Self::create_render_pipeline(&device, &ctx);
|
|
let vertex_buffer = Self::create_vertex_buffer(&device);
|
|
let texture = Self::create_texture(&device);
|
|
let texture_view = texture.create_view(&wgpu::TextureViewDescriptor {
|
|
label: "egui texture view",
|
|
..Default::default()
|
|
});
|
|
|
|
Self {
|
|
device: Arc::new(device),
|
|
queue: Arc::new(queue),
|
|
pipeline,
|
|
vertex_buffer,
|
|
texture,
|
|
texture_view,
|
|
sequence: 0,
|
|
gpu_memory_bytes: 0,
|
|
validation_enabled: true,
|
|
}
|
|
}
|
|
|
|
/// Create render pipeline - egui wgpu integration
|
|
fn create_render_pipeline(device: &Device, ctx: &Context) -> RenderPipeline {
|
|
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
|
label: "egui shader",
|
|
source: wgpu::ShaderSource::Wgui(ctx.shader_source()),
|
|
});
|
|
|
|
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
|
label: "egui pipeline layout",
|
|
bind_group_layouts: &[&Self::create_bind_group_layout(device)],
|
|
push_constant_ranges: &[],
|
|
});
|
|
|
|
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
|
label: "egui render pipeline",
|
|
layout: Some(layout),
|
|
vertex: wgpu::VertexState {
|
|
module: &module,
|
|
entry_point: "vs_main",
|
|
buffers: &[Self::vertex_buffer_layout()],
|
|
},
|
|
fragment: Some(wgpu::FragmentState {
|
|
module: &module,
|
|
entry_point: "fs_main",
|
|
targets: &[Some(wgpu::ColorTargetState {
|
|
format: wgpu::TextureFormat::Rgba8Unorm,
|
|
blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA),
|
|
write_mask: wgpu::ColorWrites::ALL,
|
|
})],
|
|
}),
|
|
primitive: wgpu::PrimitiveState {
|
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
|
strip_index_format: None,
|
|
front_face: wgpu::FrontFace::Ccw,
|
|
cull_mode: Some(wgpu::Face::Back),
|
|
unclipped_depth: false,
|
|
polygon_mode: wgpu::PolygonMode::Fill,
|
|
conservative: false,
|
|
},
|
|
depth_stencil: None,
|
|
multisample: wgpu::MultisampleState {
|
|
count: 1,
|
|
mask: !0,
|
|
sample: 0,
|
|
},
|
|
multiview: None,
|
|
})
|
|
}
|
|
|
|
/// Create bind group layout
|
|
fn create_bind_group_layout(device: &Device) -> wgpu::BindGroupLayout {
|
|
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
|
label: "egui bind group layout",
|
|
entries: &[
|
|
wgpu::BindGroupLayoutEntry {
|
|
binding: 0,
|
|
visibility: wgpu::ShaderStages::VERTEX,
|
|
ty: wgpu::BindingType::Buffer {
|
|
ty: wgpu::BufferBindingType::Uniform,
|
|
has_dynamic_offset: true,
|
|
min_binding_size: None,
|
|
},
|
|
count: None,
|
|
},
|
|
wgpu::BindGroupLayoutEntry {
|
|
binding: 1,
|
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
ty: wgpu::BindingType::Texture {
|
|
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
|
view_dimension: wgpu::TextureViewDimension::D2,
|
|
multisampled: false,
|
|
},
|
|
count: None,
|
|
},
|
|
wgpu::BindGroupLayoutEntry {
|
|
binding: 2,
|
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
|
count: None,
|
|
},
|
|
],
|
|
})
|
|
}
|
|
|
|
/// Create vertex buffer
|
|
fn create_vertex_buffer(device: &Device) -> Buffer {
|
|
device.create_buffer(&BufferInitDescriptor {
|
|
label: "egui vertex buffer",
|
|
size: 1024,
|
|
usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
|
|
mapped_at_creation: false,
|
|
})
|
|
}
|
|
|
|
/// Create texture
|
|
fn create_texture(device: &Device) -> Texture {
|
|
device.create_texture(&wgpu::TextureDescriptor {
|
|
label: "egui texture",
|
|
size: wgpu::Extent3d {
|
|
width: 256,
|
|
height: 256,
|
|
depth_or_array_layers: 1,
|
|
},
|
|
mip_level_count: 1,
|
|
sample_count: 1,
|
|
dimension: wgpu::TextureDimension::D2,
|
|
format: wgpu::TextureFormat::Rgba8Unorm,
|
|
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
|
|
view_formats: &[],
|
|
})
|
|
}
|
|
|
|
/// Vertex buffer layout
|
|
fn vertex_buffer_layout() -> wgpu::VertexBufferLayout {
|
|
wgpu::VertexBufferLayout {
|
|
array_stride: 32,
|
|
step_mode: wgpu::VertexStepMode::Vertex,
|
|
attributes: &[
|
|
wgpu::VertexAttribute {
|
|
format: wgpu::VertexFormat::Float32x2,
|
|
offset: 0,
|
|
shader_location: 0,
|
|
},
|
|
wgpu::VertexAttribute {
|
|
format: wgpu::VertexFormat::Float32x2,
|
|
offset: 8,
|
|
shader_location: 1,
|
|
},
|
|
wgpu::VertexAttribute {
|
|
format: wgpu::VertexFormat::Float32x4,
|
|
offset: 16,
|
|
shader_location: 2,
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
/// Render frame - GPU render pass
|
|
pub fn render_frame(&self, clipped_primitives: &[ClippedPrimitive], textures_delta: &TexturesDelta) {
|
|
let mut encoder = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
|
label: "egui render encoder",
|
|
});
|
|
|
|
// Upload texture deltas
|
|
for (id, image_delta) in textures_delta.set {
|
|
self.upload_texture_delta(id, image_delta, &mut encoder);
|
|
}
|
|
|
|
// Begin render pass
|
|
let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
|
|
label: "egui render pass",
|
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
|
view: &self.texture_view,
|
|
resolve_target: None,
|
|
ops: wgpu::Operations {
|
|
load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
|
|
store: wgpu::StoreOp::Store,
|
|
},
|
|
})],
|
|
depth_stencil_attachment: None,
|
|
timestamp_writes: None,
|
|
occlusion_query_set: None,
|
|
});
|
|
|
|
// Draw clipped primitives
|
|
for clipped_primitive in clipped_primitives {
|
|
self.draw_primitive(clipped_primitive, &mut render_pass);
|
|
}
|
|
|
|
// Submit commands
|
|
let commands = encoder.finish();
|
|
self.queue.submit(&[commands]);
|
|
|
|
self.sequence += 1;
|
|
log::debug!("Frame rendered: sequence {}", self.sequence);
|
|
}
|
|
|
|
/// Upload texture delta
|
|
fn upload_texture_delta(&self, id: u32, delta: egui::epaint::ImageDelta, encoder: &mut CommandEncoder) {
|
|
let pixels = delta.image.bytes.as_slice();
|
|
|
|
// GPU texture upload - direct write
|
|
encoder.write_texture(
|
|
&self.texture,
|
|
TextureAspect::All,
|
|
pixels,
|
|
ImageInfo {
|
|
offset: (delta.pos.x, delta.pos.y, 0),
|
|
size: wgpu::Extent3d {
|
|
width: delta.image.width() as u32,
|
|
height: delta.image.height() as u32,
|
|
depth_or_array_layers: 1,
|
|
},
|
|
copy_size: wgpu::Extent3d {
|
|
width: delta.image.width() as u32,
|
|
height: delta.image.height() as u32,
|
|
depth_or_array_layers: 1,
|
|
},
|
|
..Default::default()
|
|
},
|
|
);
|
|
|
|
self.gpu_memory_bytes += pixels.len() as u64;
|
|
}
|
|
|
|
/// Draw primitive
|
|
fn draw_primitive(&self, primitive: &ClippedPrimitive, render_pass: &mut wgpu::RenderPass) {
|
|
// Validate primitive before draw
|
|
if self.validation_enabled {
|
|
self.validate_primitive(primitive);
|
|
}
|
|
|
|
// Set pipeline
|
|
render_pass.set_pipeline(&self.pipeline);
|
|
|
|
// Set vertex buffer
|
|
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
|
|
|
|
// Set bind group
|
|
let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
|
label: "egui bind group",
|
|
layout: &Self::create_bind_group_layout(&self.device),
|
|
entries: &[
|
|
wgpu::BindGroupEntry {
|
|
binding: 0,
|
|
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
|
buffer: &self.vertex_buffer,
|
|
offset: 0,
|
|
size: Some(self.vertex_buffer.size()),
|
|
}),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 1,
|
|
resource: wgpu::BindingResource::TextureView(&self.texture_view),
|
|
},
|
|
wgpu::BindGroupEntry {
|
|
binding: 2,
|
|
resource: wgpu::BindingResource::Sampler(
|
|
&self.device.create_sampler(&wgpu::SamplerDescriptor::default()),
|
|
),
|
|
},
|
|
],
|
|
});
|
|
render_pass.set_bind_group(0, &bind_group, &[0]);
|
|
|
|
// Draw
|
|
render_pass.draw(0..6, 0..1);
|
|
}
|
|
|
|
/// Validate primitive - GPU validation
|
|
fn validate_primitive(&self, primitive: &ClippedPrimitive) {
|
|
// Clip rect validation
|
|
if primitive.clip_rect.width() > 4096.0 || primitive.clip_rect.height() > 4096.0 {
|
|
log::warn("Primitive clip rect too large");
|
|
}
|
|
|
|
// Vertex count validation
|
|
if primitive.primitive.primitives_count() > 10000 {
|
|
log::warn("Primitive count too high: {}", primitive.primitive.primitives_count());
|
|
}
|
|
}
|
|
|
|
/// Get GPU memory usage
|
|
pub fn gpu_memory_usage(&self) -> u64 {
|
|
self.gpu_memory_bytes
|
|
}
|
|
|
|
/// Get sequence
|
|
pub fn sequence(&self) -> u64 {
|
|
self.sequence
|
|
}
|
|
|
|
/// Is validation enabled?
|
|
pub fn is_validation_enabled(&self) -> bool {
|
|
self.validation_enabled
|
|
}
|
|
|
|
/// Toggle validation
|
|
pub fn toggle_validation(&mut self) {
|
|
self.validation_enabled = !self.validation_enabled;
|
|
log::info!("Validation: {}", self.validation_enabled);
|
|
}
|
|
|
|
/// Clear GPU buffer
|
|
pub fn clear_buffer(&mut self) {
|
|
self.gpu_memory_bytes = 0;
|
|
log::info("GPU buffer cleared");
|
|
}
|
|
}
|
|
|
|
/// TextureUsages - Texture usage flags
|
|
const TextureUsages: wgpu::TextureUsages = wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST;
|
|
|
|
/// wgpu render system - Main GPU render loop
|
|
pub fn wgpu_render_system(
|
|
mut renderer: ResMut<WgpuRenderer>,
|
|
frame_data: Res<FrameData>,
|
|
) {
|
|
// Render frame via GPU
|
|
renderer.render_frame(&frame_data.clipped_primitives, &frame_data.textures_delta);
|
|
|
|
// Log GPU memory
|
|
log::debug("GPU memory: {} bytes", renderer.gpu_memory_usage());
|
|
}
|
|
|
|
/// Frame data resource for wgpu
|
|
#[derive(Resource, Clone, Debug)]
|
|
pub struct FrameData {
|
|
pub sequence: u64,
|
|
pub clipped_primitives: Vec<ClippedPrimitive>,
|
|
pub textures_delta: TexturesDelta,
|
|
}
|
|
|
|
/// wgpu renderer marker resource
|
|
#[derive(Resource, Debug)]
|
|
pub struct WgpuRendererMarker;
|
|
|
|
/// Performance benchmark - GPU vs CPU rendering
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::time::Instant;
|
|
|
|
#[test]
|
|
fn test_gpu_render_performance() {
|
|
// Mock device/queue
|
|
let device = Device::default();
|
|
let queue = Queue::default();
|
|
let ctx = Context::default();
|
|
|
|
let renderer = WgpuRenderer::new(device, queue, ctx);
|
|
|
|
let primitives = vec![ClippedPrimitive::default(); 1000];
|
|
let textures_delta = TexturesDelta::default();
|
|
|
|
let start = Instant::now();
|
|
renderer.render_frame(&primitives, &textures_delta);
|
|
let duration = start.elapsed();
|
|
|
|
log::info!("GPU render: {}ms", duration.as_millis());
|
|
assert!(duration.as_millis() < 50, "GPU render too slow");
|
|
}
|
|
|
|
#[test]
|
|
fn test_gpu_memory_tracking() {
|
|
let device = Device::default();
|
|
let queue = Queue::default();
|
|
let ctx = Context::default();
|
|
|
|
let renderer = WgpuRenderer::new(device, queue, ctx);
|
|
|
|
assert!(renderer.gpu_memory_usage() > 0);
|
|
assert_eq!(renderer.sequence(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_primitive_validation() {
|
|
let device = Device::default();
|
|
let queue = Queue::default();
|
|
let ctx = Context::default();
|
|
|
|
let renderer = WgpuRenderer::new(device, queue, ctx);
|
|
|
|
assert!(renderer.is_validation_enabled());
|
|
|
|
let large_primitive = ClippedPrimitive {
|
|
clip_rect: egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(5000.0, 5000.0)),
|
|
primitive: egui::epaint::Primitive::default(),
|
|
};
|
|
|
|
renderer.validate_primitive(&large_primitive);
|
|
// Validation should log warning
|
|
}
|
|
} |