Merge pull request #9084 from arslanarm/shaderpacks/raw-texture-defaults

🤖 Agent: I pushed 19b0fd00 and 27d625d8: merged main and filled `Resources.packed` (#9090), `Interface.initial_values` and `Resources.shadow_linear` in the new test, which no longer compiled against current main. Raw textures defaulting to linear + clamp and PNGs to nearest + repeat match the probe table. Nothing else found.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-04 16:38:33 +01:00
commit 5d8f9fc2fc
3 files changed
+198 -14

No files matched your search

@@ -1195,9 +1195,18 @@ fn bindings(
let released = pack.released.get();
let custom = pack_textures::find(textures, group, &s.ty, &s.name).filter(|&i| {
let t = &textures[i];
t.stage != pack_textures::stage(group)
|| released.0 != t.stage
|| horizon_shaders::buffer_index(&t.name).is_none_or(|b| released.1 & 1 << b == 0)
// A PNG under `texture.<stage>.<name>` whose name is no sampler of the game's
// isn't bound: the sampler reads as if it had no texture. Iris 1.11.7 with a probe
// pack of ours (#9064) bound it over `gaux2` and `noisetex` but not over names of its
// own, where a raw texture was bound; this rule is a fit to those cases.
let unbound = t.png
&& t.stage != horizon_shaders::EVERY_STAGE
&& source(&s.name, false, true, false) == Source::Dummy;
!unbound
&& (t.stage != pack_textures::stage(group)
|| released.0 != t.stage
|| horizon_shaders::buffer_index(&t.name)
.is_none_or(|b| released.1 & 1 << b == 0))
});
let image = pack
.images
@@ -4779,6 +4788,7 @@ mod tests {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
blur: false,
clamp: false,
png: false,
image: pack_textures::Image::Atlas,
}];
let interface = horizon_shaders::Interface {
@@ -4840,6 +4850,102 @@ mod tests {
assert!(reads(Group::Final) == Source::Color(0));
}
/// Iris 1.11.7 (a probe pack of ours, #9064) didn't bind a PNG under
/// `texture.<stage>.<name>` when `name` was a name of the pack's own, no sampler of the
/// game's; a raw texture there, a PNG over `gaux2` or `noisetex`, and a `customTexture` PNG
/// were bound.
#[test]
fn a_png_under_a_name_of_its_own_is_not_bound() {
let instance = crate::gfx::test_gpu::instance();
let Ok(adapter) = pollster::block_on(instance.request_adapter(&Default::default())) else {
return;
};
let Ok((device, _queue)) = pollster::block_on(adapter.request_device(&Default::default()))
else {
return;
};
let texture = |stage: &str, name: &str, png: bool| pack_textures::PackTexture {
stage: stage.into(),
name: name.into(),
dimension: wgpu::TextureViewDimension::D2,
sample_type: wgpu::TextureSampleType::Float { filterable: true },
blur: !png,
clamp: !png,
png,
image: pack_textures::Image::Atlas,
};
let textures = [
texture("composite", "tp", true),
texture("composite", "tr", false),
texture("composite", "gaux2", true),
texture("composite", "noisetex", true),
texture(horizon_shaders::EVERY_STAGE, "cp", true),
];
let names = ["tp", "tr", "gaux2", "noisetex", "cp"];
let interface = horizon_shaders::Interface {
uniform_size: 48,
uniforms: Vec::new(),
samplers: names
.iter()
.zip(1..)
.map(|(name, k)| horizon_shaders::Sampler {
name: (*name).into(),
ty: "sampler2D".into(),
binding: 2 * k - 1,
})
.collect(),
outputs: vec![0],
draw: false,
images: Vec::new(),
buffers: Vec::new(),
initial_values: vec![0; 48],
};
let buffers = pack_images::Buffers::new(
&device,
Default::default(),
Default::default(),
wgpu::ShaderStages::FRAGMENT,
);
let stand_ins = std::cell::RefCell::new(Vec::new());
let pack = Resources {
textures: &textures,
images: &[],
left_out: &[],
buffers: &buffers,
indices: None,
gpu: Gpu::of(&adapter, device.features()),
storable: 0,
rgb: 0,
released: std::cell::Cell::new(("", 0)),
stand_ins: &stand_ins,
shadow_linear: 0,
packed: [None; BUFFERS],
};
let (_, samplers, ..) = bindings(
device.features(),
&interface,
"final",
Group::Final,
&[0],
None,
&[T::Rgba8Unorm; BUFFERS],
None,
&mut Vec::new(),
&pack,
)
.expect("the pass is kept");
let read: Vec<Source> = samplers.iter().map(|b| b.source).collect();
assert!(
read == [
Source::Dummy,
Source::Atlas,
Source::Atlas,
Source::Atlas,
Source::Atlas
]
);
}
/// Iris 1.11.7 runs a program whose image no `image.*` gives, and its loads read zero
/// (observed with a probe pack). Its pass is kept, the image bound to a stand-in of its
/// declared format, zero again before each pass even after one stored into it.
@@ -17,6 +17,8 @@ pub(super) struct PackTexture {
pub(super) sample_type: wgpu::TextureSampleType,
pub(super) blur: bool,
pub(super) clamp: bool,
/// Decoded from a PNG, the pack's or the game's (not a raw dump, not a generated texture).
pub(super) png: bool,
pub(super) image: Image,
}
@@ -30,7 +32,7 @@ pub(super) fn upload(
let features = device.features();
let mut out = Vec::new();
for t in textures {
let (image, dimension, sample_type, blur, clamp) = match t.source {
let (image, dimension, sample_type, blur, clamp, png) = match t.source {
TextureSource::Image(texture) => {
let (view, sample_type) = match create(device, queue, &texture, features) {
Ok(created) => created,
@@ -50,6 +52,7 @@ pub(super) fn upload(
sample_type,
texture.blur,
texture.clamp,
!texture.raw,
)
}
TextureSource::Resource(location) => {
@@ -66,7 +69,14 @@ pub(super) fn upload(
continue;
};
let float = wgpu::TextureSampleType::Float { filterable: true };
(image, wgpu::TextureViewDimension::D2, float, true, true)
(
image,
wgpu::TextureViewDimension::D2,
float,
true,
true,
false,
)
}
};
out.push(PackTexture {
@@ -76,6 +86,7 @@ pub(super) fn upload(
sample_type,
blur,
clamp,
png,
image,
});
}
@@ -285,6 +296,7 @@ mod tests {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
blur: false,
clamp: false,
png: false,
image: Image::Lightmap,
}
}
@@ -297,6 +309,7 @@ mod tests {
texels,
blur: false,
clamp: false,
raw: true,
}
}
+74 -9
View File
@@ -40,9 +40,12 @@ pub struct Texture {
/// The internal format the pack asked for.
pub format: Format,
pub texels: Texels,
/// From the `.mcmeta` beside it: linear filtering, and clamping instead of repeating.
/// Linear filtering, and clamping instead of repeating: on for a raw texture and off for a
/// PNG, unless the `.mcmeta` beside it says otherwise.
pub blur: bool,
pub clamp: bool,
/// A raw dump rather than a PNG.
pub raw: bool,
}
/// Texel values, `components` per texel: 1, 2 or 4 (three widen to four, alpha 1).
@@ -150,7 +153,8 @@ pub fn resolve_resources(
};
let meta = read(namespace, &format!("{path}.mcmeta"));
let text = meta.as_deref().map(String::from_utf8_lossy);
(texture.blur, texture.clamp) = mcmeta(text.as_deref());
let (blur, clamp) = mcmeta(text.as_deref());
(texture.blur, texture.clamp) = (blur.unwrap_or(false), clamp.unwrap_or(false));
t.source = TextureSource::Image(texture);
true
});
@@ -181,17 +185,19 @@ fn source(pack: &Pack, value: &str) -> Result<TextureSource, String> {
} else {
raw(&bytes, &rest)?
};
texture.blur = blur;
texture.clamp = clamp;
// A PNG is nearest and repeating, a raw texture linear and clamped (Iris 1.11.7 with a probe
// pack of ours, #9064); each key the `.mcmeta` has overrides its own default only.
texture.blur = blur.unwrap_or(texture.raw);
texture.clamp = clamp.unwrap_or(texture.raw);
Ok(TextureSource::Image(texture))
}
/// `{"texture": {"blur": true, "clamp": false}}`: both off when missing.
fn mcmeta(text: Option<&str>) -> (bool, bool) {
/// `{"texture": {"blur": true, "clamp": false}}`: each flag, or `None` when it is missing.
fn mcmeta(text: Option<&str>) -> (Option<bool>, Option<bool>) {
let flag = |key: &str| {
text.and_then(|t| t.split_once(&format!("\"{key}\"")))
.map(|(_, after)| after.trim_start().trim_start_matches(':').trim_start())
.is_some_and(|v| v.starts_with("true"))
.map(|v| v.starts_with("true"))
};
(flag("blur"), flag("clamp"))
}
@@ -233,6 +239,7 @@ pub fn decode_png(bytes: &[u8]) -> Option<Texture> {
texels: Texels::Float(rgba),
blur: false,
clamp: false,
raw: false,
})
}
@@ -347,8 +354,9 @@ fn raw(bytes: &[u8], words: &[&str]) -> Result<Texture, String> {
size,
format,
texels,
blur: false,
clamp: false,
blur: true,
clamp: true,
raw: true,
})
}
@@ -543,6 +551,63 @@ mod tests {
);
}
/// Raw textures are linear and clamped, PNGs nearest and repeating, and each key of a
/// `.mcmeta` overrides its own default only (Iris 1.11.7 with a probe pack of ours, #9064).
#[test]
fn raw_textures_are_linear_and_clamped_unless_their_mcmeta_says_otherwise() {
let mut png = Vec::new();
{
let mut e = png::Encoder::new(&mut png, 1, 1);
e.set_color(png::ColorType::Rgba);
e.set_depth(png::BitDepth::Eight);
e.write_header()
.unwrap()
.write_image_data(&[1, 2, 3, 4])
.unwrap();
}
let texel = [1u8, 2, 3, 4];
let files = [
("shaders/a.dat", texel.as_slice()),
("shaders/b.dat", texel.as_slice()),
("shaders/b.dat.mcmeta", b"{\"texture\": {\"blur\": true}}"),
("shaders/c.dat", texel.as_slice()),
("shaders/c.dat.mcmeta", b"{\"texture\": {\"clamp\": false}}"),
("shaders/d.dat", texel.as_slice()),
("shaders/d.dat.mcmeta", b"{\"texture\": {\"blur\": false}}"),
("shaders/p.png", png.as_slice()),
("shaders/q.png", png.as_slice()),
("shaders/q.png.mcmeta", b"{\"texture\": {\"blur\": true}}"),
];
let pack = Pack::from_zip_bytes("P.zip", zip(&files)).unwrap();
let raw = "TEXTURE_2D RGBA8 1 1 RGBA UNSIGNED_BYTE";
let properties = format!(
"customTexture.a = a.dat {raw}\ncustomTexture.b = b.dat {raw}\n\
texture.composite.c = c.dat {raw}\ntexture.composite.d = d.dat {raw}\n\
customTexture.p = p.png\ntexture.composite.gaux2 = q.png\n"
);
let (textures, notes) =
load_textures(&pack, &Properties::parse(&properties, &Macros::standard()));
assert!(notes.is_empty(), "{notes:?}");
let found: Vec<(&str, bool, bool)> = textures
.iter()
.map(|t| match &t.source {
TextureSource::Image(i) => (t.name.as_str(), i.blur, i.clamp),
s => panic!("{s:?}"),
})
.collect();
assert_eq!(
found,
[
("a", true, true),
("b", true, true),
("c", true, false),
("d", false, true),
("p", false, false),
("gaux2", true, false),
]
);
}
/// A texture may name one of the game's (`minecraft:textures/environment/clouds.png`): it is
/// read from the game's resources with its `.mcmeta`. The generated lightmap stays for the
/// renderer; a missing one, or a location reaching outside the resources, is dropped.