use super::*;
impl egui_wgpu::CallbackTrait for ViewportCallback {
fn paint(
&self,
_info: egui::PaintCallbackInfo,
render_pass: &mut wgpu::RenderPass<'static>,
resources: &egui_wgpu::CallbackResources,
) {
let Some(vp) = resources.get::<ViewportPaint>() else {
return;
};
render_pass.set_pipeline(&vp.pipeline);
render_pass.set_bind_group(0, &vp.bind_group, &[]);
render_pass.draw(0..3, 0..1);
}
}
impl Viewport {
pub fn new(render_state: &egui_wgpu::RenderState) -> Self {
let device = render_state.device.clone();
let queue = render_state.queue.clone();
let core = RenderCore::new(device.clone(), queue, COLOR_FORMAT);
let gpu_scene = GpuScene::default();
let target_format = render_state.target_format;
let blit_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("brep-app viewport blit"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
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: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let blit_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("brep-app viewport sampler"),
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("brep-app blit"),
source: wgpu::ShaderSource::Wgsl(blit_wgsl(target_format.is_srgb()).into()),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("brep-app blit layout"),
bind_group_layouts: &[Some(&blit_layout)],
immediate_size: 0,
});
let blit_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("brep-app blit pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs"),
compilation_options: Default::default(),
buffers: &[],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs"),
compilation_options: Default::default(),
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend: None,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
..Default::default()
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
Self {
core,
gpu_scene,
context_gpu_scene: GpuScene::default(),
egui_renderer: render_state.renderer.clone(),
blit_layout,
blit_sampler,
blit_pipeline,
offscreen: None,
force_render: true,
#[cfg(feature = "automation")]
rendered_inputs: None,
sheet: super::sheet::SheetViewport::default(),
ecad: super::ecad::EcadViewport::default(),
move_card: super::move_card::MoveCard::default(),
constraint_chips_drawn: Vec::new(),
constraint_label_menu: None,
constraint_menu_hits: std::collections::HashMap::new(),
dragging: false,
touch: Default::default(),
touch_suppressed: false,
touch_drag_offset: (0.0, 0.0),
touch_drawing: false,
chrome_obscured: false,
gizmo_dragging: false,
component_gizmo_dragging: false,
dim_dragging: None,
sketch_dragging: false,
sketch3d_dragging: false,
sketch_handdrawing: false,
last_rect: None,
candidate_popup: None,
candidate_popup_fresh: false,
hover_lit_since: None,
hover_last_pos: None,
candidate_popup_rect: None,
candidate_hits: Vec::new(),
editing_dim: None,
dim_edit_fresh: false,
editing_feature_dim: None,
feature_dim_edit_fresh: false,
constraint_dragging: false,
constraint_label_hovered: None,
pmi_label_hovered: None,
pmi_label_dragging: None,
pmi_label_hits: Vec::new(),
pmi_label_menu: None,
pmi_menu_hits: std::collections::HashMap::new(),
component_menu: None,
component_menu_hits: std::collections::HashMap::new(),
component_menu_probe: serde_json::Value::Null,
component_request: None,
empty_double_click: false,
}
}
pub(super) fn ensure_offscreen(&mut self, w: u32, h: u32) {
if let Some(off) = &self.offscreen {
if off.w == w && off.h == h {
return;
}
}
let texture = self.core.device.create_texture(&wgpu::TextureDescriptor {
label: Some("brep-app viewport offscreen"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: COLOR_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
});
let view = texture.create_view(&Default::default());
let bind_group = self.core.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("brep-app viewport blit bind group"),
layout: &self.blit_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.blit_sampler),
},
],
});
self.egui_renderer
.write()
.callback_resources
.insert(ViewportPaint {
pipeline: self.blit_pipeline.clone(),
bind_group,
});
self.offscreen = Some(Offscreen { view, w, h });
self.force_render = true;
}
#[cfg(feature = "automation")]
pub(super) fn check_stale_frame(&mut self, rendered: bool, phys_w: u32, phys_h: u32, ppp: f32, state: &EngineState) {
let mut inputs = state.render_fingerprint();
inputs[0].1 ^= (u64::from(phys_w) << 32 | u64::from(phys_h)) ^ u64::from(ppp.to_bits()).rotate_left(17);
if !rendered {
if let Some(last) = &self.rendered_inputs {
let changed: Vec<&str> = inputs.iter().zip(last).filter(|(a, b)| a.1 != b.1).map(|(a, _)| a.0).collect();
if !changed.is_empty() {
crate::perf::note_stale_frame();
eprintln!("BREP_STALE_FRAME changed={}", changed.join(","));
}
}
}
if rendered || self.rendered_inputs.is_some() {
self.rendered_inputs = Some(inputs);
}
}
pub(super) fn render_viewport(&mut self, phys_w: u32, phys_h: u32, ppp: f32, state: &mut EngineState) {
let Some(off) = &self.offscreen else { return };
{
let _span = crate::perf::span(crate::perf::Phase::Sync);
self.core.sync_scene(
&mut self.gpu_scene,
&state.scene,
&state.settings,
state.settings_generation,
);
}
self.core.sync_scene(
&mut self.context_gpu_scene, &state.context_scene,
&state.settings, state.settings_generation,
);
let (camera, overlay) = {
let _span = crate::perf::span(crate::perf::Phase::Fit);
state.fit_camera_and_overlay()
};
let params = FrameParams {
camera: &camera,
width: phys_w,
height: phys_h,
dpr: ppp,
settings: &state.settings,
emphasis: &state.emphasis,
world_per_pixel: state.camera.world_per_pixel(),
overlay: overlay.as_ref(),
};
{
let _span = crate::perf::span(crate::perf::Phase::Draw);
self.core
.render_to_view_with_context(
&mut self.gpu_scene, &state.scene, ¶ms, &off.view,
Some((&self.context_gpu_scene, &state.context_scene)),
);
}
state.dirty = false;
}
}
fn blit_wgsl(target_is_srgb: bool) -> String {
let frag_body = if target_is_srgb {
r#"
let c = textureSample(tex, samp, in.uv);
let rgb = srgb_to_linear(c.rgb);
return vec4<f32>(rgb, c.a);
"#
} else {
r#"
return textureSample(tex, samp, in.uv);
"#
};
format!(
r#"
@group(0) @binding(0) var tex: texture_2d<f32>;
@group(0) @binding(1) var samp: sampler;
struct VsOut {{
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
}};
@vertex
fn vs(@builtin(vertex_index) vi: u32) -> VsOut {{
var p = array<vec2<f32>, 3>(
vec2<f32>(-1.0, -1.0),
vec2<f32>( 3.0, -1.0),
vec2<f32>(-1.0, 3.0),
);
var out: VsOut;
let xy = p[vi];
out.pos = vec4<f32>(xy, 0.0, 1.0);
// Framebuffer origin is top-left: map clip +y (top) -> uv.y 0.
out.uv = vec2<f32>((xy.x + 1.0) * 0.5, (1.0 - xy.y) * 0.5);
return out;
}}
fn srgb_to_linear(c: vec3<f32>) -> vec3<f32> {{
let lo = c / 12.92;
let hi = pow((c + 0.055) / 1.055, vec3<f32>(2.4));
return select(hi, lo, c <= vec3<f32>(0.04045));
}}
@fragment
fn fs(in: VsOut) -> @location(0) vec4<f32> {{{frag_body}}}
"#
)
}