atagen

monolith

public
Star0
Watch0

Commit c0caa658decf

tarball

atagen <boss@atagen.co>2026-10-19 07:56 UTC

5 files changed+59-9
render-vk: skip blending for draws that cover what is under them

Every pipeline blended, including X-format textures at full alpha and opaque solid fills, which cost a destination read for nothing. Pipelines::get takes an opaque flag and frames set it for Texture draws of formats without alpha and for Solid draws at alpha one, and only when no rounded clip can lower the coverage.
Modified file: crates/render-vk/src/frame.rs+6 additions
@@ -16,6 +16,7 @@ pub(crate) enum Op {
1616 },
1717 Draw {
1818 kind: Kind,
19+ opaque: bool,
1920 texture: Option<VulkanTexture>,
2021 push: Push,
2122 scissors: Vec<vk::Rect2D>,
@@ -192,6 +193,10 @@ impl<'frame, 'buffer> VulkanFrame<'frame, 'buffer> {
192193 };
193194 self.ops.push(Op::Draw {
194195 kind,
196+ opaque: kind == Kind::Texture
197+ && alpha >= 1.0
198+ && self.clip.is_none()
199+ && texture.0.opaque(),
195200 texture: Some(texture.clone()),
196201 push,
197202 scissors,
@@ -253,6 +258,7 @@ impl Frame for VulkanFrame<'_, '_> {
253258 };
254259 self.ops.push(Op::Draw {
255260 kind: Kind::Solid,
261+ opaque: color.a() >= 1.0 && self.clip.is_none(),
256262 texture: None,
257263 push,
258264 scissors,
Modified file: crates/render-vk/src/image.rs+5 additions
@@ -167,6 +167,11 @@ impl ImageInner {
167167 Ok(Arc::new(inner))
168168 }
169169
170+ /// Whether sampling the image always gives alpha one.
171+ pub fn opaque(&self) -> bool {
172+ vk_format(self.fourcc).is_some_and(|f| f.opaque)
173+ }
174+
170175 pub fn layout(&self) -> vk::ImageLayout {
171176 *self.layout.lock().unwrap_or_else(|e| e.into_inner())
172177 }
Modified file: crates/render-vk/src/pipeline.rs+21 additions−8 deletions
@@ -47,7 +47,7 @@ pub(crate) struct Pipelines {
4747 solid: vk::ShaderModule,
4848 texture: vk::ShaderModule,
4949 effect: vk::ShaderModule,
50- cache: HashMap<(vk::Format, Kind), vk::Pipeline>,
50+ cache: HashMap<(vk::Format, Kind, bool), vk::Pipeline>,
5151 vk_cache: vk::PipelineCache,
5252 cache_path: Option<PathBuf>,
5353 loaded_len: usize,
@@ -135,9 +135,15 @@ impl Pipelines {
135135 let Some(format) = vk_format(fourcc) else {
136136 continue;
137137 };
138- for kind in [Kind::Solid, Kind::Texture, Kind::Effect] {
139- if let Err(e) = self.get(format.format, kind) {
140- tracing::warn!(?kind, "cannot warm a pipeline: {e}");
138+ for (kind, opaque) in [
139+ (Kind::Solid, false),
140+ (Kind::Solid, true),
141+ (Kind::Texture, false),
142+ (Kind::Texture, true),
143+ (Kind::Effect, false),
144+ ] {
145+ if let Err(e) = self.get(format.format, kind, opaque) {
146+ tracing::warn!(?kind, opaque, "cannot warm a pipeline: {e}");
141147 return;
142148 }
143149 }
@@ -180,8 +186,15 @@ impl Pipelines {
180186 }
181187 }
182188
183- pub fn get(&mut self, format: vk::Format, kind: Kind) -> Result<vk::Pipeline, Error> {
184- if let Some(&p) = self.cache.get(&(format, kind)) {
189+ /// The pipeline for `kind` into `format`. An `opaque` one replaces the
190+ /// destination instead of blending, for draws known to cover it.
191+ pub fn get(
192+ &mut self,
193+ format: vk::Format,
194+ kind: Kind,
195+ opaque: bool,
196+ ) -> Result<vk::Pipeline, Error> {
197+ if let Some(&p) = self.cache.get(&(format, kind, opaque)) {
185198 return Ok(p);
186199 }
187200 let d = &self.shared.device.device;
@@ -213,7 +226,7 @@ impl Pipelines {
213226 let multisample = vk::PipelineMultisampleStateCreateInfo::default()
214227 .rasterization_samples(vk::SampleCountFlags::TYPE_1);
215228 let attachments = [vk::PipelineColorBlendAttachmentState::default()
216- .blend_enable(true)
229+ .blend_enable(!opaque)
217230 .src_color_blend_factor(vk::BlendFactor::ONE)
218231 .dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA)
219232 .color_blend_op(vk::BlendOp::ADD)
@@ -240,7 +253,7 @@ impl Pipelines {
240253 .push_next(&mut rendering);
241254 let pipeline = unsafe { d.create_graphics_pipelines(self.vk_cache, &[info], None) }
242255 .map_err(|(_, e)| e)?[0];
243- self.cache.insert((format, kind), pipeline);
256+ self.cache.insert((format, kind, opaque), pipeline);
244257 Ok(pipeline)
245258 }
246259 }
Modified file: crates/render-vk/src/renderer/mod.rs+3 additions−1 deletions
@@ -429,7 +429,9 @@ impl VulkanRenderer {
429429 let mut pipelines = Vec::with_capacity(ops.len());
430430 for op in &ops {
431431 pipelines.push(match op {
432- Op::Draw { kind, .. } => Some(self.pipelines.get(image.format, *kind)?),
432+ Op::Draw { kind, opaque, .. } => {
433+ Some(self.pipelines.get(image.format, *kind, *opaque)?)
434+ }
433435 Op::Clear { .. } => None,
434436 });
435437 }
Modified file: crates/render-vk/tests/golden.rs+24 additions
@@ -275,3 +275,27 @@ fn queued_uploads_to_one_texture_land_in_order() {
275275 });
276276 assert_eq!(at(&s.pixels(), 32, 32), RED);
277277 }
278+
279+#[test]
280+fn opaque_format_texture_replaces_what_is_under_it() {
281+ let mut s = Scene::new();
282+ let bytes = [0u8, 255, 0, 0];
283+ let tex = s
284+ .renderer
285+ .import_memory(&bytes, Fourcc::Xbgr8888, (1, 1).into(), false)
286+ .unwrap();
287+ s.draw(Transform::Normal, |f| {
288+ f.clear(rgba(RED), &[full()]).unwrap();
289+ f.render_texture_from_to(
290+ &tex,
291+ Rectangle::from_size((1.0, 1.0).into()),
292+ full(),
293+ &[full()],
294+ &[],
295+ Transform::Normal,
296+ 1.0,
297+ )
298+ .unwrap();
299+ });
300+ assert_eq!(at(&s.pixels(), 32, 32), GREEN);
301+}