perf: reduce memory usage\nAllocate textures only when needed

This commit is contained in:
2026-09-24 22:18:44 +02:00
parent 38cbb1f132
commit 9b0dec25e2
+47 -25
View File
@@ -27,6 +27,16 @@ pub const MAX_REF_POINTS: usize = 1 << 17;
/// no `float32-filterable` feature is needed. /// no `float32-filterable` feature is needed.
const DATA_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float; const DATA_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float;
/// Cap on the interactive cache's pixel count (the texture is scaled down,
/// aspect kept, above it). Each pixel costs 36 bytes across the data, AA and
/// colour textures, and the 3D view renders at 2× per axis, so a HiDPI screen
/// in 3D would otherwise want 0.5 GB+. Browsers cap WebGPU memory well below
/// what native gets, so the web budget is ~4K (≈300 MB); native, ~8K.
#[cfg(target_arch = "wasm32")]
const MAX_CACHE_PIXELS: u32 = 3840 * 2160;
#[cfg(not(target_arch = "wasm32"))]
const MAX_CACHE_PIXELS: u32 = 7680 * 4320;
/// True when the two uniforms differ in any field the iteration pass depends on /// True when the two uniforms differ in any field the iteration pass depends on
/// (i.e. anything except the palette / colour scale / offset / camera). /// (i.e. anything except the palette / colour scale / offset / camera).
fn geom_differs(a: &Uniforms, b: &Uniforms) -> bool { fn geom_differs(a: &Uniforms, b: &Uniforms) -> bool {
@@ -216,21 +226,21 @@ pub struct Uniforms {
/// Offscreen textures for the two-pass render, recreated whenever the widget's /// Offscreen textures for the two-pass render, recreated whenever the widget's
/// pixel size changes: /// pixel size changes:
/// * `data_view` — the 1-spp iteration pass's output (see [`DATA_FORMAT`]). /// * `data_view` — the 1-spp iteration pass's output (see [`DATA_FORMAT`]).
/// * `data_aa_view` — the adaptive-AA refine pass's output (only when AA is on). /// * `aa` — the adaptive-AA refine pass's output (only when AA is on).
/// * `color_view` — the colourise pass's output; the blit source. /// * `color_view` — the colourise pass's output; the blit source.
/// plus the bind groups that read them. /// plus the bind groups that read them.
struct CacheTarget { struct CacheTarget {
/// Kept so they can be `destroy()`ed on resize (see `ensure_cache`). /// Kept so they can be `destroy()`ed on resize (see `ensure_cache`).
textures: [wgpu::Texture; 3], textures: Vec<wgpu::Texture>,
data_view: wgpu::TextureView, data_view: wgpu::TextureView,
data_aa_view: wgpu::TextureView,
color_view: wgpu::TextureView, color_view: wgpu::TextureView,
/// Refine pass input (group 1): the 1-spp data texture. /// Refine pass input (group 1): the 1-spp data texture.
refine_bind_group: wgpu::BindGroup, refine_bind_group: wgpu::BindGroup,
/// Colourise pass input: uniforms + the 1-spp data texture. /// Colourise pass input: uniforms + the 1-spp data texture.
colorize_bind_group: wgpu::BindGroup, colorize_bind_group: wgpu::BindGroup,
/// Colourise pass input when AA is on: uniforms + the refined texture. /// Refine pass output + the colourise bind group reading it. Only
colorize_aa_bind_group: wgpu::BindGroup, /// allocated while AA is on: it's a second full-size `Rgba32Float`.
aa: Option<(wgpu::TextureView, wgpu::BindGroup)>,
/// Blit pass input: the colour texture + sampler. /// Blit pass input: the colour texture + sampler.
blit_bind_group: wgpu::BindGroup, blit_bind_group: wgpu::BindGroup,
width: u32, width: u32,
@@ -595,12 +605,14 @@ impl FractalRenderer {
} }
} }
/// Ensure the cache texture exists at `width`×`height`. Recreates it (and its /// Ensure the cache textures exist at `width`×`height` (plus the AA refine
/// blit bind group) on a size change, invalidating any previous render. /// target iff `aa`). Recreates them (and their bind groups) on a change,
fn ensure_cache(&mut self, device: &wgpu::Device, width: u32, height: u32) { /// invalidating any previous render.
fn ensure_cache(&mut self, device: &wgpu::Device, width: u32, height: u32, aa: bool) {
if let Some(c) = &self.cache if let Some(c) = &self.cache
&& c.width == width && c.width == width
&& c.height == height && c.height == height
&& c.aa.is_some() == aa
{ {
return; return;
} }
@@ -638,17 +650,19 @@ impl FractalRenderer {
let data_view = data_texture.create_view(&wgpu::TextureViewDescriptor::default()); let data_view = data_texture.create_view(&wgpu::TextureViewDescriptor::default());
// Adaptive-AA output: same format, written by the refine pass. // Adaptive-AA output: same format, written by the refine pass.
let data_aa_texture = device.create_texture(&wgpu::TextureDescriptor { let data_aa_texture = aa.then(|| {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("fractal data (AA)"), label: Some("fractal data (AA)"),
size: extent, size: extent,
mip_level_count: 1, mip_level_count: 1,
sample_count: 1, sample_count: 1,
dimension: wgpu::TextureDimension::D2, dimension: wgpu::TextureDimension::D2,
format: DATA_FORMAT, format: DATA_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[], view_formats: &[],
})
}); });
let data_aa_view = data_aa_texture.create_view(&wgpu::TextureViewDescriptor::default());
// Colour texture (colourise output; blit source). // Colour texture (colourise output; blit source).
let color_texture = device.create_texture(&wgpu::TextureDescriptor { let color_texture = device.create_texture(&wgpu::TextureDescriptor {
@@ -684,7 +698,11 @@ impl FractalRenderer {
}) })
}; };
let colorize_bind_group = colorize_bind_group_for(&data_view); let colorize_bind_group = colorize_bind_group_for(&data_view);
let colorize_aa_bind_group = colorize_bind_group_for(&data_aa_view); let aa_target = data_aa_texture.as_ref().map(|t| {
let view = t.create_view(&wgpu::TextureViewDescriptor::default());
let bind_group = colorize_bind_group_for(&view);
(view, bind_group)
});
let refine_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { let refine_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("refine bind group"), label: Some("refine bind group"),
@@ -711,13 +729,15 @@ impl FractalRenderer {
}); });
self.cache = Some(CacheTarget { self.cache = Some(CacheTarget {
textures: [data_texture, data_aa_texture, color_texture], textures: [Some(data_texture), data_aa_texture, Some(color_texture)]
.into_iter()
.flatten()
.collect(),
data_view, data_view,
data_aa_view,
color_view, color_view,
refine_bind_group, refine_bind_group,
colorize_bind_group, colorize_bind_group,
colorize_aa_bind_group, aa: aa_target,
blit_bind_group, blit_bind_group,
width, width,
height, height,
@@ -1315,12 +1335,16 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
// (the iterate pass maps pixels through NDC, so the view is unchanged; // (the iterate pass maps pixels through NDC, so the view is unchanged;
// the blit just upsamples). The 2× 3D supersample on a large/HiDPI // the blit just upsamples). The 2× 3D supersample on a large/HiDPI
// screen can otherwise exceed it. // screen can otherwise exceed it.
// Also cap the total pixel count (`MAX_CACHE_PIXELS`), same way.
let max_dim = device.limits().max_texture_dimension_2d; let max_dim = device.limits().max_texture_dimension_2d;
let [w, h] = self.size_px.map(|v| v.max(1)); let [w, h] = self.size_px.map(|v| v.max(1));
let scale = (max_dim as f64 / w.max(h) as f64).min(1.0); let scale = (max_dim as f64 / w.max(h) as f64)
.min((MAX_CACHE_PIXELS as f64 / (w as f64 * h as f64)).sqrt())
.min(1.0);
let width = ((w as f64 * scale) as u32).clamp(1, max_dim); let width = ((w as f64 * scale) as u32).clamp(1, max_dim);
let height = ((h as f64 * scale) as u32).clamp(1, max_dim); let height = ((h as f64 * scale) as u32).clamp(1, max_dim);
renderer.ensure_cache(device, width, height); let aa = self.uniforms.aa_level > 1;
renderer.ensure_cache(device, width, height, aa);
// Iteration (expensive) re-runs only when the geometry inputs change; // Iteration (expensive) re-runs only when the geometry inputs change;
// colourise (cheap) re-runs when it did, or when only a colour/camera/ // colourise (cheap) re-runs when it did, or when only a colour/camera/
@@ -1372,7 +1396,6 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
renderer.uploaded_lights = Some(self.lights); renderer.uploaded_lights = Some(self.lights);
} }
let aa = self.uniforms.aa_level > 1;
if iter_dirty { if iter_dirty {
renderer.ensure_pipelines(device, PipelineKey::from_uniforms(&self.uniforms)); renderer.ensure_pipelines(device, PipelineKey::from_uniforms(&self.uniforms));
} }
@@ -1387,12 +1410,12 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
&pipelines.iterate, &pipelines.iterate,
&[&renderer.bind_group], &[&renderer.bind_group],
); );
if aa { if let Some((data_aa_view, _)) = &cache.aa {
// Adaptive AA: supersample only the non-smooth pixels. // Adaptive AA: supersample only the non-smooth pixels.
data_pass( data_pass(
egui_encoder, egui_encoder,
"fractal AA refine pass", "fractal AA refine pass",
&cache.data_aa_view, data_aa_view,
&pipelines.refine, &pipelines.refine,
&[&renderer.bind_group, &cache.refine_bind_group], &[&renderer.bind_group, &cache.refine_bind_group],
); );
@@ -1400,11 +1423,10 @@ impl egui_wgpu::CallbackTrait for FractalCallback {
} }
// Colourise pass: data texture → colour texture. // Colourise pass: data texture → colour texture.
let colorize_bind_group = if aa { let colorize_bind_group = cache
&cache.colorize_aa_bind_group .aa
} else { .as_ref()
&cache.colorize_bind_group .map_or(&cache.colorize_bind_group, |(_, bg)| bg);
};
data_pass( data_pass(
egui_encoder, egui_encoder,
"fractal colorize pass", "fractal colorize pass",