perf: keep heavy work off UI thread
This commit is contained in:
parent
12bf126646
commit
deec48ad99
75 changed files with 11703 additions and 519 deletions
|
|
@ -47,7 +47,7 @@ pub struct Pipeline {
|
|||
/// Used to draw ghost copies of selected wires through occluding geometry.
|
||||
wire_xray_pipeline: wgpu::RenderPipeline,
|
||||
/// Layout for the per-wire `WireConst` storage buffer (group 1 of the wire /
|
||||
/// xray pipelines). `Some` only on the fast native path; `None` in packed
|
||||
/// xray pipelines). `Some` on native/WebGPU fast paths; `None` in packed
|
||||
/// compatibility mode. Passed to `WireGpu::from_run` / `from_batch`.
|
||||
pub(crate) wire_const_bgl: Option<wgpu::BindGroupLayout>,
|
||||
wipeout_pipeline: wgpu::RenderPipeline,
|
||||
|
|
@ -338,8 +338,8 @@ impl Pipeline {
|
|||
});
|
||||
|
||||
// ── Wire pipeline ──────────────────────────────────────────────────
|
||||
// Select once per device. The fast native path hoists shared constants
|
||||
// into storage; compatibility mode keeps them in 10 packed attributes.
|
||||
// Select once per device. Native and WebGPU hoist shared constants into
|
||||
// storage when limits allow; WebGL2/compat keeps packed attributes.
|
||||
let wire_mode = wire_gpu::WirePipelineMode::select(device);
|
||||
let renderer_mode_name =
|
||||
if wire_mode.uses_storage() { "fast-storage" } else { "packed-compat" };
|
||||
|
|
@ -357,12 +357,9 @@ impl Pipeline {
|
|||
renderer_mode_name,
|
||||
device.limits().max_storage_buffers_per_shader_stage
|
||||
);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let wire_const_bgl = wire_mode
|
||||
.uses_storage()
|
||||
.then(|| wire_gpu::WireConst::bind_group_layout(device));
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let wire_const_bgl: Option<wgpu::BindGroupLayout> = None;
|
||||
let mut wire_bgls: Vec<&wgpu::BindGroupLayout> = vec![&frame_bgl];
|
||||
if let Some(bgl) = &wire_const_bgl {
|
||||
wire_bgls.push(bgl);
|
||||
|
|
@ -379,7 +376,6 @@ impl Pipeline {
|
|||
let wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||
label: Some("wire.shader"),
|
||||
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(match wire_mode {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
wire_gpu::WirePipelineMode::IndexedStorage => {
|
||||
include_str!("../../shaders/wire_indexed.wgsl")
|
||||
}
|
||||
|
|
@ -3157,6 +3153,14 @@ fn create_msaa_texture(
|
|||
pub struct MultiPipeline {
|
||||
pub(crate) inners: Vec<Pipeline>,
|
||||
format: wgpu::TextureFormat,
|
||||
/// Stable viewport identity → pipeline slot map. Paper viewports used to
|
||||
/// occupy slots by their current list position, so switching layouts or
|
||||
/// scrolling a sheet could assign an existing viewport to a different
|
||||
/// slot and throw away all of its GPU caches. Keep the association across
|
||||
/// tab switches and only recycle genuinely cold slots.
|
||||
pub(crate) slot_by_instance: rustc_hash::FxHashMap<u64, usize>,
|
||||
slot_last_used: Vec<u64>,
|
||||
slot_clock: u64,
|
||||
/// The resident wire batches, keyed by `wire_content_id` and shared across
|
||||
/// every slot (and every pane — one `MultiPipeline` backs all of them) that
|
||||
/// renders the same content. `prepare` builds an entry once on a cache miss
|
||||
|
|
@ -3189,8 +3193,70 @@ impl MultiPipeline {
|
|||
let n = n.max(1);
|
||||
while self.inners.len() < n {
|
||||
self.inners.push(Pipeline::new(device, queue, self.format));
|
||||
self.slot_last_used.push(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve stable slots for the viewport identities in one primitive.
|
||||
/// Thirty-two hot slots cover ordinary tiled/paper drawings. A cold slot
|
||||
/// is recycled only after several other prepare calls, which prevents
|
||||
/// sibling Model panes prepared in the same frame from evicting each
|
||||
/// other. If every slot is still hot, growing is safer than a visible
|
||||
/// rebuild hitch.
|
||||
pub(crate) fn resolve_slots(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
instance_ids: &[u64],
|
||||
) -> Vec<usize> {
|
||||
const SOFT_LIMIT: usize = 32;
|
||||
const HOT_WINDOW: u64 = 8;
|
||||
|
||||
self.slot_clock = self.slot_clock.wrapping_add(1).max(1);
|
||||
let now = self.slot_clock;
|
||||
let reserved: rustc_hash::FxHashSet<u64> = instance_ids.iter().copied().collect();
|
||||
let mut slots = Vec::with_capacity(instance_ids.len());
|
||||
|
||||
for &instance_id in instance_ids {
|
||||
let slot = if let Some(&slot) = self.slot_by_instance.get(&instance_id) {
|
||||
slot
|
||||
} else {
|
||||
let vacant = self
|
||||
.inners
|
||||
.iter()
|
||||
.position(|inner| inner.slot_id == u64::MAX);
|
||||
let recyclable = vacant.or_else(|| {
|
||||
(self.inners.len() >= SOFT_LIMIT)
|
||||
.then(|| {
|
||||
self.inners
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, inner)| !reserved.contains(&inner.slot_id))
|
||||
.filter(|(slot, _)| {
|
||||
now.saturating_sub(self.slot_last_used[*slot]) > HOT_WINDOW
|
||||
})
|
||||
.min_by_key(|(slot, _)| self.slot_last_used[*slot])
|
||||
.map(|(slot, _)| slot)
|
||||
})
|
||||
.flatten()
|
||||
});
|
||||
let slot = recyclable.unwrap_or_else(|| {
|
||||
let slot = self.inners.len();
|
||||
self.ensure_len(device, queue, slot + 1);
|
||||
slot
|
||||
});
|
||||
let old_id = self.inners[slot].slot_id;
|
||||
if old_id != u64::MAX {
|
||||
self.slot_by_instance.remove(&old_id);
|
||||
}
|
||||
self.slot_by_instance.insert(instance_id, slot);
|
||||
slot
|
||||
};
|
||||
self.slot_last_used[slot] = now;
|
||||
slots.push(slot);
|
||||
}
|
||||
slots
|
||||
}
|
||||
}
|
||||
|
||||
/// Send wgpu's uncaptured validation errors to stderr instead of the default
|
||||
|
|
@ -3227,6 +3293,9 @@ impl iced::widget::shader::Pipeline for MultiPipeline {
|
|||
Self {
|
||||
inners: vec![Pipeline::new(device, queue, format)],
|
||||
format,
|
||||
slot_by_instance: rustc_hash::FxHashMap::default(),
|
||||
slot_last_used: vec![0],
|
||||
slot_clock: 0,
|
||||
wire_buffer_cache: rustc_hash::FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,18 +58,17 @@ fn instance_buffer_mapped<T: bytemuck::Pod>(
|
|||
|
||||
// ── Instance layout ───────────────────────────────────────────────────────
|
||||
|
||||
// ── Native: slim per-segment instance + shared per-wire constants ───────────
|
||||
// ── WebGPU/native: slim per-segment instance + shared constants ─────────────
|
||||
//
|
||||
// Every segment of a wire used to carry the wire's color / line-weight / dash
|
||||
// pattern / draw-depth (~44 B) on each instance — re-fetched once per segment
|
||||
// even though it's constant along the wire. On native we hoist those into a
|
||||
// even though it's constant along the wire. On storage-capable adapters we hoist those into a
|
||||
// per-wire `WireConst` storage buffer indexed by `wire_id`, so the instance
|
||||
// keeps only the per-segment data (endpoints + arc-length distances). Cuts the
|
||||
// instance from 104 B to one 64-byte cache line and removes the redundant
|
||||
// per-segment re-fetch of the shared constants. WebGL2 has no vertex-stage
|
||||
// storage buffers, so the wasm build below keeps the original self-contained
|
||||
// fat instance.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct WireInstance {
|
||||
|
|
@ -87,8 +86,6 @@ pub struct WireInstance {
|
|||
/// exactly one 64-byte cache line.
|
||||
pub taper_ratio: [u16; 2],
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl WireInstance {
|
||||
pub fn layout<'a>() -> wgpu::VertexBufferLayout<'a> {
|
||||
// Must match `InstanceIn` in wire_indexed.wgsl.
|
||||
|
|
@ -110,11 +107,10 @@ impl WireInstance {
|
|||
}
|
||||
}
|
||||
|
||||
/// Per-wire constants shared by every segment of a wire (native only). std430
|
||||
/// Per-wire constants shared by every segment of a wire. std430
|
||||
/// layout: three vec4 then eight scalars = 80 B, matching `WireConst` in
|
||||
/// wire_indexed.wgsl. `align_end` / `align_total` carry the "A"-type endpoint
|
||||
/// alignment (see `wire_distances`); 0.0 total = no alignment.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct WireConst {
|
||||
|
|
@ -134,8 +130,6 @@ pub struct WireConst {
|
|||
pub _pad1: f32,
|
||||
pub _pad2: f32,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl WireConst {
|
||||
/// Bind-group layout for the per-wire storage buffer (group 1 of the wire /
|
||||
/// xray pipelines). Read-only storage, visible to the vertex stage.
|
||||
|
|
@ -158,8 +152,8 @@ impl WireConst {
|
|||
|
||||
// ── Packed compatibility instance (no vertex-stage storage) ────────────────
|
||||
//
|
||||
// Web always uses this layout. Native selects it at runtime for adapters whose
|
||||
// storage-buffer limits are insufficient, or when --compat-renderer is set.
|
||||
// WebGL2 and limited native adapters use this layout. WebGPU selects the
|
||||
// indexed storage path when its reported limits satisfy the same requirement.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct PackedWireInstance {
|
||||
|
|
@ -224,21 +218,15 @@ impl PackedWireInstance {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub type WireInstance = PackedWireInstance;
|
||||
|
||||
/// Wire and hatch pipelines switch together: the fast path uses storage
|
||||
/// buffers; the compatibility path carries wire constants in packed vertex
|
||||
/// attributes and hatch data in a texture.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WirePipelineMode {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
IndexedStorage,
|
||||
Packed,
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn select_native_pipeline(max_storage_buffers_per_stage: u32, forced: bool) -> WirePipelineMode {
|
||||
fn select_pipeline(max_storage_buffers_per_stage: u32, forced: bool) -> WirePipelineMode {
|
||||
const REQUIRED_STORAGE_BUFFERS_PER_STAGE: u32 = 5;
|
||||
if forced || max_storage_buffers_per_stage < REQUIRED_STORAGE_BUFFERS_PER_STAGE {
|
||||
WirePipelineMode::Packed
|
||||
|
|
@ -249,23 +237,15 @@ fn select_native_pipeline(max_storage_buffers_per_stage: u32, forced: bool) -> W
|
|||
|
||||
impl WirePipelineMode {
|
||||
pub fn select(device: &wgpu::Device) -> Self {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = device;
|
||||
Self::Packed
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
select_native_pipeline(
|
||||
device.limits().max_storage_buffers_per_shader_stage,
|
||||
crate::cli::gui_config().compat_renderer,
|
||||
)
|
||||
}
|
||||
let forced = crate::cli::gui_config().compat_renderer;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let forced = false;
|
||||
select_pipeline(device.limits().max_storage_buffers_per_shader_stage, forced)
|
||||
}
|
||||
|
||||
pub fn uses_storage(self) -> bool {
|
||||
match self {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Self::IndexedStorage => true,
|
||||
Self::Packed => false,
|
||||
}
|
||||
|
|
@ -273,7 +253,6 @@ impl WirePipelineMode {
|
|||
|
||||
pub fn layout<'a>(self) -> wgpu::VertexBufferLayout<'a> {
|
||||
match self {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
Self::IndexedStorage => WireInstance::layout(),
|
||||
Self::Packed => PackedWireInstance::layout(),
|
||||
}
|
||||
|
|
@ -494,9 +473,8 @@ fn emit_wire_packed(
|
|||
instances
|
||||
}
|
||||
|
||||
/// Native: emit slim per-segment instances (positions + distances + `wire_id`)
|
||||
/// Storage path: emit slim per-segment instances (positions + distances + `wire_id`)
|
||||
/// plus the one `WireConst` record every segment of this wire shares.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn emit_wire_native(
|
||||
wire: &WireModel,
|
||||
wire_id: u32,
|
||||
|
|
@ -579,11 +557,10 @@ pub(crate) fn wire_draw_depth(
|
|||
}
|
||||
}
|
||||
|
||||
/// Build the shared per-wire `WireConst` storage buffer and its bind group
|
||||
/// (native only). All instance-buffer chunks from one build reference the same
|
||||
/// Build the shared per-wire `WireConst` storage buffer and its bind group.
|
||||
/// All instance-buffer chunks from one build reference the same
|
||||
/// buffer via their global `wire_id`, so a single bind group is cloned into
|
||||
/// each chunk.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn build_const_bind_group(
|
||||
device: &wgpu::Device,
|
||||
bgl: &wgpu::BindGroupLayout,
|
||||
|
|
@ -677,7 +654,6 @@ impl WireGpu {
|
|||
mesh_edge: bool,
|
||||
const_bgl: Option<&wgpu::BindGroupLayout>,
|
||||
) -> Vec<Self> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(const_bgl) = const_bgl {
|
||||
const MAX_INSTANCES: usize =
|
||||
268_435_456 / std::mem::size_of::<WireInstance>();
|
||||
|
|
@ -769,7 +745,6 @@ impl WireGpu {
|
|||
if total_segs == 0 {
|
||||
return vec![];
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(const_bgl) = const_bgl {
|
||||
// GPU max buffer size is 256 MB; chunk to stay within the limit.
|
||||
const MAX_INSTANCES: usize =
|
||||
|
|
|
|||
Loading…
Reference in a new issue