refactor(viewport): Primitive carries Vec<ViewportData> + MultiPipeline (step 2-3/8)

Birleşik viewport mimarisinin yapısal temeli. Primitive artık tek kamera/
geometri yerine viewport başına bir ViewportData listesi taşıyor; yeni
MultiPipeline her viewport için bir iç Pipeline instance tutuyor.
prepare/render viewport listesi üzerinde dönüp her iç-pipeline'ı kendi
(normalized) ekran rect'ine çiziyor.

Mevcut Pipeline kodu (upload/LOD/render/blit) HİÇ değişmedi — sadece
viewport başına bir kez çalışıyor. build_primitive ve
build_viewport_primitive tek-elemanlı liste (FULL_VIEWPORT_RECT)
ürettiği için render davranışı tek-viewport ile birebir aynı; çoklu
viewport sonraki adımda (view.rs tek widget + active_viewports) aktive
edilecek.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-27 19:47:42 +03:00
commit b073421b92
2 changed files with 170 additions and 88 deletions

View file

@ -1835,8 +1835,38 @@ fn create_msaa_texture(
})
}
impl iced::widget::shader::Pipeline for Pipeline {
fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
Self::new(device, queue, format)
/// Holds one inner `Pipeline` per viewport drawn this frame. A single
/// shader widget owns one `MultiPipeline`; the unified renderer grows the
/// `inners` vector to match the viewport count and draws each into its own
/// screen rectangle. Inner `Pipeline` code (upload / LOD / render / blit)
/// is unchanged — it just runs once per viewport.
pub struct MultiPipeline {
pub(crate) inners: Vec<Pipeline>,
format: wgpu::TextureFormat,
}
impl MultiPipeline {
/// Ensure exactly `n` (≥1) inner pipelines exist, creating any missing
/// ones. Extra pipelines beyond `n` are dropped.
pub(crate) fn ensure_len(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
n: usize,
) {
let n = n.max(1);
while self.inners.len() < n {
self.inners.push(Pipeline::new(device, queue, self.format));
}
self.inners.truncate(n);
}
}
impl iced::widget::shader::Pipeline for MultiPipeline {
fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
Self {
inners: vec![Pipeline::new(device, queue, format)],
format,
}
}
}

View file

@ -12,19 +12,19 @@ use iced::{Rectangle, Size};
use std::sync::Arc;
use super::pipeline::viewcube::{hover_id, VIEWCUBE_PX};
use super::pipeline::Pipeline;
use super::pipeline::MultiPipeline;
use super::tess_util;
use super::{HatchModel, ImageModel, MeshLodSet, Scene, Uniforms, WireModel};
// ── PaperViewportPipeline / PaperViewportPrimitive ────────────────────────
//
// Newtype wrappers around Pipeline / Primitive so that the active-MSPACE
// Newtype wrappers around MultiPipeline / Primitive so that the active-MSPACE
// viewport widget gets its own Iced storage entry (keyed by TypeId of the
// Pipeline type). This prevents the shared-pipeline prepare() overwrite
// that occurs when PaperSheet and the viewport widget both use `Pipeline`.
// that occurs when PaperSheet and the viewport widget both use the same type.
/// Dedicated pipeline for the MSPACE active-viewport shader widget.
pub struct PaperViewportPipeline(pub(super) Pipeline);
pub struct PaperViewportPipeline(pub(super) MultiPipeline);
impl iced::widget::shader::Pipeline for PaperViewportPipeline {
fn new(
@ -32,7 +32,9 @@ impl iced::widget::shader::Pipeline for PaperViewportPipeline {
queue: &iced::wgpu::Queue,
format: iced::wgpu::TextureFormat,
) -> Self {
Self(Pipeline::new(device, queue, format))
Self(<MultiPipeline as iced::widget::shader::Pipeline>::new(
device, queue, format,
))
}
}
@ -76,8 +78,21 @@ pub struct CameraState {
// ── GPU primitive ─────────────────────────────────────────────────────────
/// Normalized rectangle covering the whole widget — used for a single
/// full-window viewport (Model view or active paper viewport).
const FULL_VIEWPORT_RECT: Rectangle = Rectangle {
x: 0.0,
y: 0.0,
width: 1.0,
height: 1.0,
};
/// Everything needed to render one viewport: its geometry, camera, render
/// mode, and the screen rectangle it occupies. The unified renderer carries
/// a `Vec<ViewportData>` (one per tiled / floating viewport); each gets its
/// own inner `Pipeline` instance drawn into its own rectangle.
#[derive(Debug)]
pub struct Primitive {
pub struct ViewportData {
pub(super) wires: Arc<Vec<WireModel>>,
/// 3DFACE entity wires — separated so they are uploaded to the dedicated
/// face3d pipeline (fill + batched edges) instead of N individual WireGpu.
@ -92,8 +107,6 @@ pub struct Primitive {
/// Used by the ViewCube pipeline — no gimbal lock.
pub(super) cam_rotation: Mat4,
pub(super) hover_region: Option<usize>,
/// Background color used to clear the MSAA buffer at the start of each frame.
pub(super) bg_color: [f32; 4],
pub(super) show_viewcube: bool,
/// Header.fill_mode (FILLMODE): when false, hatch / wipeout / face3d-fill
/// uploads short-circuit so the renderer draws only wireframe.
@ -121,6 +134,20 @@ pub struct Primitive {
/// with `geometry_epoch` so the wire buffers re-upload when the view
/// changes (frustum culling produces a different wire list).
pub(super) camera_generation: u64,
/// Screen rectangle this viewport fills, **normalized** to the widget
/// bounds (each component in 0..1). A single full-widget view is
/// `(0, 0, 1, 1)`; tiled / floating viewports are sub-rectangles.
/// Normalized form lets `render()` derive the physical sub-clip from
/// the surface clip without needing the scale factor.
pub(super) screen_rect: Rectangle,
}
#[derive(Debug)]
pub struct Primitive {
/// One entry per viewport drawn this frame (≥1).
pub(super) viewports: Vec<ViewportData>,
/// Background color used to clear each viewport's MSAA buffer.
pub(super) bg_color: [f32; 4],
}
/// Flags the render pipeline consumes, derived from
@ -196,11 +223,11 @@ pub fn render_mode_flags(
// ── shader::Primitive impl ────────────────────────────────────────────────
impl shader::Primitive for Primitive {
type Pipeline = Pipeline;
type Pipeline = MultiPipeline;
fn prepare(
&self,
pipeline: &mut Pipeline,
pipeline: &mut MultiPipeline,
device: &iced::wgpu::Device,
queue: &iced::wgpu::Queue,
bounds: &Rectangle,
@ -208,90 +235,107 @@ impl shader::Primitive for Primitive {
) {
let phys = viewport.physical_size();
let full_size = Size::new(phys.width, phys.height);
// MSAA and depth textures are sized to the shader widget's clip bounds,
// not the full surface — so the MSAA resolve can't overwrite other widgets.
let scale = viewport.scale_factor() as f32;
let clip_size = Size::new(
(bounds.width * scale).ceil() as u32,
(bounds.height * scale).ceil() as u32,
);
pipeline.ensure_depth_texture(device, clip_size);
pipeline.viewcube.ensure_depth_texture(device, full_size);
pipeline.upload_uniforms(queue, &self.uniforms);
let cur_key = (self.geometry_epoch, self.camera_generation);
let fill_mode = self.fill_mode;
// 3D face fill requires *both* the doc-level FILLMODE *and* the
// per-view Solid toggle. Hatches / wipeouts deliberately ignore
// the view toggle so 2D fills stay on even when the user picks
// the Wireframe overlay style.
let face3d_fill_active = fill_mode && !self.view_wireframe;
if cur_key != pipeline.cached_epoch {
// Static buffers (hatches/images/meshes) only need refresh on a
// real geometry change, not on every camera tick.
if self.geometry_epoch != pipeline.cached_epoch.0 {
if fill_mode {
pipeline.upload_hatches(device, &self.hatches[..]);
pipeline.upload_wipeouts(device, &self.wipeout_hatches[..]);
} else {
pipeline.upload_hatches(device, &[]);
pipeline.upload_wipeouts(device, &[]);
pipeline.ensure_len(device, queue, self.viewports.len());
for (i, vp) in self.viewports.iter().enumerate() {
let inner = &mut pipeline.inners[i];
// Physical MSAA/depth size for this viewport's sub-rect.
let clip_size = Size::new(
(vp.screen_rect.width * bounds.width * scale).ceil().max(1.0) as u32,
(vp.screen_rect.height * bounds.height * scale).ceil().max(1.0) as u32,
);
inner.ensure_depth_texture(device, clip_size);
inner.viewcube.ensure_depth_texture(device, full_size);
inner.upload_uniforms(queue, &vp.uniforms);
let cur_key = (vp.geometry_epoch, vp.camera_generation);
let fill_mode = vp.fill_mode;
// 3D face fill requires *both* the doc-level FILLMODE *and* the
// per-view Solid toggle. Hatches / wipeouts deliberately ignore
// the view toggle so 2D fills stay on even when the user picks
// the Wireframe overlay style.
let face3d_fill_active = fill_mode && !vp.view_wireframe;
if cur_key != inner.cached_epoch {
// Static buffers (hatches/images/meshes) only need refresh on
// a real geometry change, not on every camera tick.
if vp.geometry_epoch != inner.cached_epoch.0 {
if fill_mode {
inner.upload_hatches(device, &vp.hatches[..]);
inner.upload_wipeouts(device, &vp.wipeout_hatches[..]);
} else {
inner.upload_hatches(device, &[]);
inner.upload_wipeouts(device, &[]);
}
inner.upload_images(device, queue, &vp.images[..]);
inner.upload_meshes(device, &vp.meshes[..]);
}
pipeline.upload_images(device, queue, &self.images[..]);
pipeline.upload_meshes(device, &self.meshes[..]);
// Wires re-upload on every camera change because the visible
// subset shifts under frustum culling.
inner.upload_wires(device, &vp.wires[..]);
inner.upload_face3d(
device,
&vp.face3d_wires[..],
&vp.wires[..],
!face3d_fill_active,
);
inner.cached_epoch = cur_key;
}
let vproj = vp.uniforms.view_proj;
inner.compute_wire_scissors(vproj, clip_size.width, clip_size.height);
inner.compute_wipeout_scissors(vproj, clip_size.width, clip_size.height);
inner.compute_image_scissors(vproj, clip_size.width, clip_size.height);
inner.compute_hatch_lod(queue, vproj, clip_size.width, clip_size.height);
inner.compute_wipeout_lod(vproj, clip_size.width, clip_size.height);
inner.compute_mesh_lod(vproj, clip_size.width, clip_size.height);
if vp.show_viewcube {
inner.viewcube.upload(
queue,
vp.cam_rotation,
(vp.screen_rect.width * bounds.width) as u32,
(vp.screen_rect.height * bounds.height) as u32,
vp.hover_region,
);
}
// Wires re-upload on every camera change because the visible
// subset shifts under frustum culling.
pipeline.upload_wires(device, &self.wires[..]);
// `wireframe_only=true` keeps the face3d edge buffer but
// drops the fill — that's the on-screen result of toggling
// the render mode to Wireframe2D / Wireframe3D.
pipeline.upload_face3d(
device,
&self.face3d_wires[..],
&self.wires[..],
!face3d_fill_active,
);
pipeline.cached_epoch = cur_key;
}
pipeline.compute_wire_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
pipeline.compute_wipeout_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
pipeline.compute_image_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
pipeline.compute_hatch_lod(queue, self.uniforms.view_proj, clip_size.width, clip_size.height);
pipeline.compute_wipeout_lod(self.uniforms.view_proj, clip_size.width, clip_size.height);
pipeline.compute_mesh_lod(self.uniforms.view_proj, clip_size.width, clip_size.height);
if self.show_viewcube {
pipeline.viewcube.upload(
queue,
self.cam_rotation,
bounds.width as u32,
bounds.height as u32,
self.hover_region,
);
}
}
fn render(
&self,
pipeline: &Pipeline,
pipeline: &MultiPipeline,
encoder: &mut iced::wgpu::CommandEncoder,
target: &iced::wgpu::TextureView,
clip: &Rectangle<u32>,
) {
// `mesh_fill` is false for Wireframe 2D / Wireframe 3D — flip
// the draw path so meshes use the wireframe pipeline + the
// pre-built triangle-edge index buffer.
let mesh_wireframe = !self.mesh_fill;
pipeline.render(
encoder,
target,
*clip,
self.bg_color,
mesh_wireframe,
self.hidden_line,
self.show_3d_edges,
);
if self.show_viewcube {
pipeline.viewcube.render(encoder, target, *clip);
for (i, vp) in self.viewports.iter().enumerate() {
let Some(inner) = pipeline.inners.get(i) else {
break;
};
// Derive this viewport's physical sub-clip from the normalized
// screen_rect and the surface clip.
let cw = clip.width as f32;
let ch = clip.height as f32;
let vp_clip = Rectangle {
x: clip.x + (vp.screen_rect.x * cw) as u32,
y: clip.y + (vp.screen_rect.y * ch) as u32,
width: (vp.screen_rect.width * cw).max(1.0) as u32,
height: (vp.screen_rect.height * ch).max(1.0) as u32,
};
// `mesh_fill` is false for Wireframe 2D / Wireframe 3D — flip
// the draw path so meshes use the wireframe pipeline + the
// pre-built triangle-edge index buffer.
let mesh_wireframe = !vp.mesh_fill;
inner.render(
encoder,
target,
vp_clip,
self.bg_color,
mesh_wireframe,
vp.hidden_line,
vp.show_3d_edges,
);
if vp.show_viewcube {
inner.viewcube.render(encoder, target, vp_clip);
}
}
}
}
@ -480,7 +524,7 @@ impl Scene {
let mut uniforms = Uniforms::new(&cam, bounds, self.document.header.lineweight_display);
uniforms.flat_shade = if flags.flat_shade { 1.0 } else { 0.0 };
Primitive {
let data = ViewportData {
wires: all_wires,
face3d_wires: Arc::new(face3d_wires),
hatches: self.hatch_models_arc(),
@ -490,7 +534,6 @@ impl Scene {
uniforms,
cam_rotation: cam.view_rotation_mat(),
hover_region,
bg_color,
show_viewcube,
fill_mode: self.document.header.fill_mode,
view_wireframe,
@ -499,6 +542,11 @@ impl Scene {
hidden_line: flags.hidden_line,
geometry_epoch: self.geometry_epoch,
camera_generation: self.camera_generation,
screen_rect: FULL_VIEWPORT_RECT,
};
Primitive {
viewports: vec![data],
bg_color,
}
}
@ -541,7 +589,7 @@ impl Scene {
let mut uniforms = Uniforms::new(&cam, bounds, self.document.header.lineweight_display);
uniforms.flat_shade = if flags.flat_shade { 1.0 } else { 0.0 };
Primitive {
let data = ViewportData {
wires: all_wires,
face3d_wires: Arc::new(face3d_wires),
hatches: self.hatch_models_arc(),
@ -551,7 +599,6 @@ impl Scene {
uniforms,
cam_rotation: cam.view_rotation_mat(),
hover_region,
bg_color: self.bg_color,
show_viewcube,
fill_mode: self.document.header.fill_mode,
view_wireframe,
@ -560,6 +607,11 @@ impl Scene {
hidden_line: flags.hidden_line,
geometry_epoch: self.geometry_epoch,
camera_generation: self.camera_generation,
screen_rect: FULL_VIEWPORT_RECT,
};
Primitive {
viewports: vec![data],
bg_color: self.bg_color,
}
}