Files
airdef/horizon-patches/0013-HD-bind-groups-cached-until-a-resize-or-a-new-source.patch
T
chexit ccc3e20902
build / mod (push) Successful in 4m4s
release / windows (push) Successful in 23m13s
Playtest fixes: smoke by engine, searchlights, far launch sites that load their chunks, air defence balance, last chance and radar-cued shots, radars in smart raids, echo, Windows release build from a tag
Smoke: one rule by engine, no heat on trails, no trail on P1-SUN. Searchlights: two lamps a target, off with an empty sky. Halos fade before their reach and their sum is limited. Launch sites load chunks through the server thread, go off-map as a last resort, and reach 4000 blocks. Air defence: ranges x1.5, shots grow with urgency, claims released, /airdef lastchance, long shots on a big radar's tracks. Build: rust-toolchain.toml, airdef.py check, generator manifest and --check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 23:44:14 +03:00

440 lines
17 KiB
Diff

From 7a5a38f68923aec62f10bfa0df50b436474ed612 Mon Sep 17 00:00:00 2001
From: chexit <chexit@duck.com>
Date: Thu, 29 Oct 2026 16:19:53 +0300
Subject: [PATCH 13/40] HD: bind groups cached until a resize or a new source,
shadow passes draw only the entities that cast shadows
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
crates/horizon-client/src/gfx.rs | 39 ++++-
crates/horizon-client/src/gfx/hd.rs | 247 +++++++++++++++++-----------
2 files changed, 188 insertions(+), 98 deletions(-)
diff --git a/crates/horizon-client/src/gfx.rs b/crates/horizon-client/src/gfx.rs
index e351763fd..f6bbe038c 100644
--- a/crates/horizon-client/src/gfx.rs
+++ b/crates/horizon-client/src/gfx.rs
@@ -459,6 +459,8 @@ pub struct Renderer {
entity_buffer: wgpu::Buffer,
entity_capacity: u64,
entity_batch: EntityBatch,
+ /// AirDef fork: the runs of entity quads (first, end) that cast shadows this frame.
+ shadow_casters: Vec<(u32, u32)>,
group: wgpu::BindGroup,
globals: wgpu::Buffer,
lightmap: wgpu::Texture,
@@ -2744,6 +2746,7 @@ impl Renderer {
entity_buffer,
entity_capacity,
entity_batch: EntityBatch::default(),
+ shadow_casters: Vec::new(),
group,
globals,
lightmap,
@@ -4980,6 +4983,30 @@ impl Renderer {
}
// The sun's (or moon's) shadow cascades: the world's globals swapped for the light's.
if shadow_strength > 0.0 {
+ // The quads the shadow shader would not discard (overlays 4, 9, 10 and 40:
+ // glowing mod geometry), as runs, so those are not vertex-processed twice.
+ self.shadow_casters.clear();
+ let mut start = None;
+ let quads = self.entity_batch.combined.as_chunks::<4>().0;
+ for (i, quad) in quads.iter().take(entity_quads as usize).enumerate() {
+ let caster = !matches!(quad[0].layer, 4 | 9 | 10 | 40);
+ match (caster, start) {
+ (true, None) => start = Some(i as u32),
+ (false, Some(first)) => {
+ self.shadow_casters.push((first, i as u32));
+ start = None;
+ }
+ _ => {}
+ }
+ }
+ if let Some(first) = start {
+ self.shadow_casters.push((first, entity_quads));
+ }
+ // Many tiny runs cost more in draw calls than they save: draw everything.
+ if self.shadow_casters.len() > 64 {
+ self.shadow_casters.clear();
+ self.shadow_casters.push((0, entity_quads));
+ }
self.queue
.write_buffer(&hd.main_globals, 0, bytemuck::bytes_of(&globals));
for (c, cascade) in hd_cascades.iter().enumerate() {
@@ -5021,11 +5048,13 @@ impl Renderer {
let instance = hd_shadow_first + k as u32;
pass.draw_indexed(0..s.solid * 6, s.start as i32, instance..instance + 1);
}
- if entity_quads > 0 {
+ if !self.shadow_casters.is_empty() {
pass.set_pipeline(&self.shadow_entity);
pass.set_bind_group(0, &self.entity_group, &[]);
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
- pass.draw_indexed(0..entity_quads * 6, 0, 0..1);
+ for &(first, end) in &self.shadow_casters {
+ pass.draw_indexed(first * 6..end * 6, 0, 0..1);
+ }
}
}
encoder.copy_buffer_to_buffer(&hd.main_globals, 0, &self.globals, 0, globals_size);
@@ -5586,7 +5615,7 @@ impl Renderer {
);
} else if let Some((color, depth)) = &scaled
&& hd_on
- && let Some(hd) = &self.hd
+ && let Some(hd) = &mut self.hd
{
// AirDef fork: lit by the mods' lights into the HD target, the mod's post pass on
// that, then bloom and the tone curve onto the screen.
@@ -5607,9 +5636,9 @@ impl Renderer {
self.mod_params.1,
);
post.run(&self.device, &mut encoder, (&hd.lit, depth), &hd.fx);
- hd.finish(&self.device, &mut encoder, &hd.fx, &view_target);
+ hd.finish(&self.device, &mut encoder, hd::HdSource::Fx, &view_target);
}
- _ => hd.finish(&self.device, &mut encoder, &hd.lit, &view_target),
+ _ => hd.finish(&self.device, &mut encoder, hd::HdSource::Lit, &view_target),
}
} else if let Some((color, depth)) = &scaled
&& mod_post
diff --git a/crates/horizon-client/src/gfx/hd.rs b/crates/horizon-client/src/gfx/hd.rs
index c36c1795d..fe2634f2c 100644
--- a/crates/horizon-client/src/gfx/hd.rs
+++ b/crates/horizon-client/src/gfx/hd.rs
@@ -359,6 +359,21 @@ fn target(
Target { view }
}
+/// The outside views and sampler the lighting group reads.
+type LightKey = (
+ wgpu::TextureView,
+ wgpu::TextureView,
+ wgpu::TextureView,
+ wgpu::Sampler,
+);
+
+/// What bloom and the tone curve read: the lit world, or the post pass's output.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum HdSource {
+ Lit,
+ Fx,
+}
+
pub struct Hd {
size: [u32; 2],
pub albedo: wgpu::TextureView,
@@ -377,6 +392,15 @@ pub struct Hd {
pub lit: wgpu::TextureView,
pub fx: wgpu::TextureView,
bloom: Vec<Target>,
+ /// Bind groups, rebuilt only when their views change: `resize` drops all of them, the
+ /// lighting and the source-reading ones are keyed by the views they read.
+ light_group: Option<(LightKey, wgpu::BindGroup)>,
+ prefilter_group: Option<(wgpu::TextureView, wgpu::BindGroup)>,
+ tone_group: Option<(wgpu::TextureView, wgpu::BindGroup)>,
+ /// Reading bloom level k - 1 (for the down pass into level k, index k - 1) and level k (for
+ /// the up pass onto level k - 1, index k - 1).
+ down_groups: Vec<wgpu::BindGroup>,
+ up_groups: Vec<wgpu::BindGroup>,
frame: wgpu::Buffer,
lights: wgpu::Buffer,
sampler: wgpu::Sampler,
@@ -656,6 +680,11 @@ impl Hd {
lit: target(device, "HD lit", [1, 1], HDR).view,
fx: target(device, "HD post", [1, 1], HDR).view,
bloom: Vec::new(),
+ light_group: None,
+ prefilter_group: None,
+ tone_group: None,
+ down_groups: Vec::new(),
+ up_groups: Vec::new(),
frame,
lights,
sampler,
@@ -690,6 +719,16 @@ impl Hd {
target(device, "HD bloom", s, HDR)
})
.collect();
+ // Every group names a view that was just replaced.
+ self.light_group = None;
+ self.prefilter_group = None;
+ self.tone_group = None;
+ self.down_groups = (1..self.bloom.len())
+ .map(|k| self.bloom_group(device, &self.bloom[k - 1].view))
+ .collect();
+ self.up_groups = (1..self.bloom.len())
+ .map(|k| self.bloom_group(device, &self.bloom[k].view))
+ .collect();
}
/// This frame's camera, sun, fog, lights and settings.
@@ -829,73 +868,85 @@ impl Hd {
/// Lights the world (its colour and depth) into `lit`.
pub fn light(
- &self,
+ &mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
world: (&wgpu::TextureView, &wgpu::TextureView),
lightmap: (&wgpu::TextureView, &wgpu::Sampler),
) {
- let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("HD lighting"),
- layout: &self.light_layout,
- entries: &[
- wgpu::BindGroupEntry {
- binding: 0,
- resource: self.frame.as_entire_binding(),
- },
- wgpu::BindGroupEntry {
- binding: 1,
- resource: wgpu::BindingResource::TextureView(world.0),
- },
- wgpu::BindGroupEntry {
- binding: 2,
- resource: wgpu::BindingResource::TextureView(world.1),
- },
- wgpu::BindGroupEntry {
- binding: 3,
- resource: wgpu::BindingResource::TextureView(&self.albedo),
- },
- wgpu::BindGroupEntry {
- binding: 4,
- resource: wgpu::BindingResource::TextureView(&self.normal),
- },
- wgpu::BindGroupEntry {
- binding: 5,
- resource: wgpu::BindingResource::TextureView(&self.depth),
- },
- wgpu::BindGroupEntry {
- binding: 6,
- resource: self.lights.as_entire_binding(),
- },
- wgpu::BindGroupEntry {
- binding: 7,
- resource: wgpu::BindingResource::TextureView(&self.levels),
- },
- wgpu::BindGroupEntry {
- binding: 8,
- resource: wgpu::BindingResource::TextureView(&self.shadow_array),
- },
- wgpu::BindGroupEntry {
- binding: 9,
- resource: wgpu::BindingResource::Sampler(&self.shadow_sampler),
- },
- wgpu::BindGroupEntry {
- binding: 10,
- resource: wgpu::BindingResource::TextureView(lightmap.0),
- },
- wgpu::BindGroupEntry {
- binding: 11,
- resource: wgpu::BindingResource::Sampler(lightmap.1),
- },
- ],
- });
+ let key = (
+ world.0.clone(),
+ world.1.clone(),
+ lightmap.0.clone(),
+ lightmap.1.clone(),
+ );
+ if self.light_group.as_ref().is_none_or(|(k, _)| *k != key) {
+ let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("HD lighting"),
+ layout: &self.light_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: self.frame.as_entire_binding(),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::TextureView(world.0),
+ },
+ wgpu::BindGroupEntry {
+ binding: 2,
+ resource: wgpu::BindingResource::TextureView(world.1),
+ },
+ wgpu::BindGroupEntry {
+ binding: 3,
+ resource: wgpu::BindingResource::TextureView(&self.albedo),
+ },
+ wgpu::BindGroupEntry {
+ binding: 4,
+ resource: wgpu::BindingResource::TextureView(&self.normal),
+ },
+ wgpu::BindGroupEntry {
+ binding: 5,
+ resource: wgpu::BindingResource::TextureView(&self.depth),
+ },
+ wgpu::BindGroupEntry {
+ binding: 6,
+ resource: self.lights.as_entire_binding(),
+ },
+ wgpu::BindGroupEntry {
+ binding: 7,
+ resource: wgpu::BindingResource::TextureView(&self.levels),
+ },
+ wgpu::BindGroupEntry {
+ binding: 8,
+ resource: wgpu::BindingResource::TextureView(&self.shadow_array),
+ },
+ wgpu::BindGroupEntry {
+ binding: 9,
+ resource: wgpu::BindingResource::Sampler(&self.shadow_sampler),
+ },
+ wgpu::BindGroupEntry {
+ binding: 10,
+ resource: wgpu::BindingResource::TextureView(lightmap.0),
+ },
+ wgpu::BindGroupEntry {
+ binding: 11,
+ resource: wgpu::BindingResource::Sampler(lightmap.1),
+ },
+ ],
+ });
+ self.light_group = Some((key, group));
+ }
+ let Some((_, group)) = &self.light_group else {
+ return;
+ };
Self::draw(
encoder,
"HD lighting",
&self.lit,
false,
&self.light_pipeline,
- &group,
+ group,
);
}
@@ -922,12 +973,53 @@ impl Hd {
/// Bloom from `source` (the lit world or the post pass's output), then the tone curve onto `screen`.
pub fn finish(
- &self,
+ &mut self,
device: &wgpu::Device,
encoder: &mut wgpu::CommandEncoder,
- source: &wgpu::TextureView,
+ source: HdSource,
screen: &wgpu::TextureView,
) {
+ let source = match source {
+ HdSource::Lit => self.lit.clone(),
+ HdSource::Fx => self.fx.clone(),
+ };
+ if self
+ .prefilter_group
+ .as_ref()
+ .is_none_or(|(v, _)| *v != source)
+ {
+ let group = self.bloom_group(device, &source);
+ self.prefilter_group = Some((source.clone(), group));
+ }
+ if self.tone_group.as_ref().is_none_or(|(v, _)| *v != source) {
+ let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("HD tone mapping"),
+ layout: &self.tone_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: self.frame.as_entire_binding(),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: wgpu::BindingResource::TextureView(&source),
+ },
+ wgpu::BindGroupEntry {
+ binding: 2,
+ resource: wgpu::BindingResource::TextureView(&self.bloom[0].view),
+ },
+ wgpu::BindGroupEntry {
+ binding: 3,
+ resource: wgpu::BindingResource::Sampler(&self.sampler),
+ },
+ ],
+ });
+ self.tone_group = Some((source, group));
+ }
+ let (Some((_, prefilter)), Some((_, tone))) = (&self.prefilter_group, &self.tone_group)
+ else {
+ return;
+ };
// Down: the prefilter into the first level, then each level from the one above.
Self::draw(
encoder,
@@ -935,61 +1027,30 @@ impl Hd {
&self.bloom[0].view,
false,
&self.prefilter,
- &self.bloom_group(device, source),
+ prefilter,
);
for k in 1..self.bloom.len() {
- let g = self.bloom_group(device, &self.bloom[k - 1].view);
Self::draw(
encoder,
"HD bloom down",
&self.bloom[k].view,
false,
&self.down,
- &g,
+ &self.down_groups[k - 1],
);
}
// Up: each level added onto the bigger one.
for k in (1..self.bloom.len()).rev() {
- let g = self.bloom_group(device, &self.bloom[k].view);
Self::draw(
encoder,
"HD bloom up",
&self.bloom[k - 1].view,
true,
&self.up,
- &g,
+ &self.up_groups[k - 1],
);
}
- let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
- label: Some("HD tone mapping"),
- layout: &self.tone_layout,
- entries: &[
- wgpu::BindGroupEntry {
- binding: 0,
- resource: self.frame.as_entire_binding(),
- },
- wgpu::BindGroupEntry {
- binding: 1,
- resource: wgpu::BindingResource::TextureView(source),
- },
- wgpu::BindGroupEntry {
- binding: 2,
- resource: wgpu::BindingResource::TextureView(&self.bloom[0].view),
- },
- wgpu::BindGroupEntry {
- binding: 3,
- resource: wgpu::BindingResource::Sampler(&self.sampler),
- },
- ],
- });
- Self::draw(
- encoder,
- "HD tone mapping",
- screen,
- false,
- &self.tone,
- &group,
- );
+ Self::draw(encoder, "HD tone mapping", screen, false, &self.tone, tone);
}
}
--
2.54.0 (Apple Git-157)