Merge pull request #7460 from arslanarm/shaderpacks/composite-scale

🤖 Agent: queued at ec8cebc6a8. A scaled pass loads its attachments instead of clearing them and draws into a whole-pixel viewport, so its buffers keep what lies outside it. Unscaled programs keep [1,0,0] and draw as before. Checked in a union with main: clippy -D warnings, all horizon-client/client-lib/items lib tests and datapack_worldgen parity pass.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-02 21:10:24 +01:00
commit bc761767d3
2 files changed
+136 -2

No files matched your search

@@ -380,6 +380,9 @@ struct Pass {
image_copies: u64,
/// The pack's images a compute pass's loads read as they were before it: copied first.
snapshots: u64,
/// `scale.<program>` (scale, offset x, offset y): the pass draws only that part of its
/// buffers and keeps the rest.
viewport: Option<[f32; 3]>,
}
impl Pass {
@@ -892,6 +895,20 @@ fn target(format: wgpu::TextureFormat) -> Option<wgpu::ColorTargetState> {
})
}
/// The viewport of `scale.<program>` (scale, offset x, offset y) on buffers of `size`, in whole
/// pixels, at least one and inside them. Buffer rows are GL's, so the offsets count from GL's
/// bottom-left corner as they are.
fn viewport(size: [u32; 2], [scale, x, y]: [f32; 3]) -> [f32; 4] {
let [w, h] = size.map(|n| n as f32);
let (x, y) = ((x * w).floor().min(w - 1.0), (y * h).floor().min(h - 1.0));
[
x,
y,
(scale * w).floor().clamp(1.0, w - x),
(scale * h).floor().clamp(1.0, h - y),
]
}
fn attach(
view: &wgpu::TextureView,
ops: wgpu::Operations<wgpu::Color>,
@@ -1555,6 +1572,7 @@ fn build_pass(
compute: Vec::new(),
sink,
geometry: p.geometry_triangles,
viewport: (!final_pass && p.scale != [1.0, 0.0, 0.0]).then_some(p.scale),
image_copies,
snapshots: 0,
})
@@ -1688,6 +1706,7 @@ fn build_compute(
geometry: 0,
image_copies,
snapshots,
viewport: None,
})
}
@@ -3990,18 +4009,27 @@ impl PackPost {
.map(|t| &t[usize::from(self.main & 1 << i == 0)])
};
let mut attachments: Vec<Option<wgpu::RenderPassColorAttachment>> = Vec::new();
// A scaled pass keeps what its buffers hold outside its viewport.
let ops = if pass.viewport.is_some() {
wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}
} else {
overwrite
};
if final_pass {
attachments.push(attach(screen, overwrite));
shown = true;
} else if pass.sink {
attachments.push(attach(&targets.sink, overwrite));
attachments.push(attach(&targets.sink, ops));
} else {
for &(location, buffer) in &pass.writes {
let l = location as usize;
if attachments.len() <= l {
attachments.resize(l + 1, None);
}
attachments[l] = view(buffer).and_then(|v| attach(v, overwrite));
attachments[l] = view(buffer).and_then(|v| attach(v, ops));
}
}
{
@@ -4018,6 +4046,10 @@ impl PackPost {
{
rp.set_bind_group(4, g, &[]);
}
if let Some(scale) = pass.viewport {
let [x, y, w, h] = viewport(out, scale);
rp.set_viewport(x, y, w, h, 0.0, 1.0);
}
rp.draw(0..6, 0..1);
}
self.main ^= pass.flip;
@@ -4273,6 +4305,27 @@ mod tests {
assert_eq!(&read.slice(..).get_mapped_range().unwrap()[..], &[0; 4]);
}
/// `scale.composite2 = 0.25 0.5 0.25` on a 1280×720 picture: Iris 1.11.7 draws x 640..960
/// and GL y 180..360 (a probe pack of our own). Rows are GL's here, so that is rows 180..360.
#[test]
fn a_scaled_pass_draws_its_part_from_gl_s_corner() {
assert_eq!(
viewport([1280, 720], [0.25, 0.5, 0.25]),
[640.0, 180.0, 320.0, 180.0]
);
assert_eq!(
viewport([1280, 720], [0.5, 0.0, 0.0]),
[0.0, 0.0, 640.0, 360.0]
);
// Whole pixels, at least one, and never past the buffer's edge.
assert_eq!(viewport([5, 3], [0.5, 0.0, 0.0]), [0.0, 0.0, 2.0, 1.0]);
assert_eq!(viewport([4, 4], [0.01, 0.0, 0.0]), [0.0, 0.0, 1.0, 1.0]);
assert_eq!(
viewport([100, 100], [1.0, 0.5, 0.75]),
[50.0, 75.0, 50.0, 25.0]
);
}
#[test]
fn pack_formats_map_to_renderable_ones() {
let f = |name| texture_format(Format::parse(name).unwrap(), wgpu::Features::empty());
+81
View File
@@ -226,6 +226,9 @@ pub struct PostProgram {
/// A geometry stage folded into the vertex stage (`translate_geometry`): drawn non-indexed,
/// six vertices per quad, with this many instances. 0 without one.
pub geometry_triangles: u32,
/// `scale.<name>`: (scale, offset x, offset y), the part of its buffers a program draws, as
/// fractions of their size from GL's bottom-left corner; [1, 0, 0] (all of them) without.
pub scale: [f32; 3],
}
/// `size.buffer.<buffer>`: a size of its own, relative to the render size (floats) or in
@@ -951,6 +954,7 @@ impl Loader<'_> {
mipmaps: 0,
compute,
geometry_triangles: 0,
scale: FULL,
});
}
if !enabled(
@@ -1031,6 +1035,7 @@ impl Loader<'_> {
Some(PostProgram {
buffer_blends: buffer_blends(self.properties, &name),
flips: flips(self.properties, &name),
scale: scale(self.properties, &name, group),
name,
group,
program: Some(program),
@@ -1130,6 +1135,7 @@ impl Loader<'_> {
Some(PostProgram {
buffer_blends: buffer_blends(self.properties, &name),
flips: flips(self.properties, &name),
scale: scale(self.properties, &name, group),
name,
group,
program: Some(program),
@@ -1270,6 +1276,29 @@ fn buffer_blends(properties: &Properties, program: &str) -> Vec<(u32, Option<Ble
.collect()
}
/// A program drawing all of its buffers.
const FULL: [f32; 3] = [1.0, 0.0, 0.0];
/// `scale.<program>=<scale> [<offsetX> <offsetY>]` of a deferred or composite program (OptiFine's
/// "Composite render scale"); prepare programs are treated the same, unverified. Iris 1.11.7
/// applies the offsets too (a probe pack of our own, composite). A value that is not a scale in (0, 1] with offsets in [0, 1) draws all of them.
fn scale(properties: &Properties, program: &str, group: Group) -> [f32; 3] {
if !matches!(group, Group::Prepare | Group::Deferred | Group::Composite) {
return FULL;
}
let Some(value) = properties.get(&format!("scale.{program}")) else {
return FULL;
};
let numbers: Option<Vec<f32>> = value.split_whitespace().map(|n| n.parse().ok()).collect();
let fraction = |s: f32| s > 0.0 && s <= 1.0;
let offset = |o: f32| (0.0..1.0).contains(&o);
match numbers.as_deref() {
Some(&[s]) if fraction(s) => [s, 0.0, 0.0],
Some(&[s, x, y]) if fraction(s) && offset(x) && offset(y) => [s, x, y],
_ => FULL,
}
}
/// `flip.<program>.<buffer>=<bool>` keys of one program.
fn flips(properties: &Properties, program: &str) -> Vec<(u32, bool)> {
let prefix = format!("flip.{program}.");
@@ -1525,6 +1554,58 @@ mod tests {
assert!(post.programs.iter().any(|p| p.name == "composite3"));
}
#[test]
fn scale_keys_give_composite_style_programs_a_viewport() {
let names = [
"deferred",
"composite",
"composite1",
"composite2",
"composite3",
"final",
];
let files: Vec<(String, &str)> = names
.iter()
.flat_map(|n| {
[
(format!("shaders/{n}.vsh"), VSH),
(format!("shaders/{n}.fsh"), FSH),
]
})
.collect();
let files: Vec<(&str, &str)> = files.iter().map(|(n, t)| (n.as_str(), *t)).collect();
let pack = Pack::from_zip_bytes("t", zip(&files)).unwrap();
let properties = Properties::parse(
"scale.deferred = 0.5\nscale.composite = 0.25 0.5 0.25\nscale.composite1 = 2\n\
scale.composite2 = 0.5 0.5\nscale.final = 0.5\n",
&Macros::default(),
);
let post = load_post(
&pack,
"",
&properties,
&Macros::standard(),
&GameInputs::default(),
);
let scales: Vec<(&str, [f32; 3])> = post
.programs
.iter()
.map(|p| (p.name.as_str(), p.scale))
.collect();
assert_eq!(
scales,
[
("deferred", [0.5, 0.0, 0.0]),
("composite", [0.25, 0.5, 0.25]),
// Not a scale, an offset missing, a program that draws the screen: all of it.
("composite1", FULL),
("composite2", FULL),
("composite3", FULL),
("final", FULL),
]
);
}
#[test]
fn image_declarations_get_their_format_once() {
let csh = |decl: &str| {