Skip to content

Commit ab6f5d9

Browse files
authored
feat(lambda-rs): Add anisotropic clamping to texture samplers (#187)
## Summary Expose anisotropic filtering on samplers so users can opt into higher-quality texture sampling at oblique viewing angles (with a configurable quality/perf tradeoff). ## Related Issues - Resolves #103 ## Changes - Add `anisotropy_clamp: u16` (default `1`) + `with_anisotropy_clamp(u16)` to `lambda-rs-platform` `SamplerBuilder`, plumbed to `wgpu::SamplerDescriptor::anisotropy_clamp`. - Clamp requested anisotropy to `[1, 16]` and disable anisotropy when unsupported by the adapter (or when filters are not all linear, per wgpu validation rules). - Document the quality/performance tradeoff and include example usage. - Add unit tests verifying default + clamping behavior. - Add a high-level `lambda-rs` `SamplerBuilder::with_anisotropy_clamp(u16)` that forwards to the platform builder. ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] Feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation (updates to docs, specs, tutorials, or comments) - [ ] Refactor (code change that neither fixes a bug nor adds a feature) - [ ] Performance (change that improves performance) - [x] Test (adding or updating tests) - [ ] Build/CI (changes to build process or CI configuration) ## Affected Crates - [x] `lambda-rs` - [x] `lambda-rs-platform` - [ ] `lambda-rs-args` - [ ] `lambda-rs-logging` - [ ] Other: ## Checklist - [x] Code follows the repository style guidelines (`cargo +nightly fmt --all`) - [ ] Code passes clippy (`cargo clippy --workspace --all-targets -- -D warnings`) - [ ] Tests pass (`cargo test --workspace`) - [x] New code includes appropriate documentation - [x] Public API changes are documented - [ ] Breaking changes are noted in this PR description ## Testing **Commands run:** ```bash cargo +nightly fmt --all cargo test -p lambda-rs-platform cargo test -p lambda-rs ``` **Manual verification steps (if applicable):** 1. (Optional) Run a textured demo and confirm angled surfaces show reduced shimmering/blurring when using `with_anisotropy_clamp(8)` with linear + mipmapped sampling. ## Screenshots/Recordings N/A ## Platform Testing - [x] macOS - [ ] Windows - [ ] Linux ## Additional Notes - wgpu requires all filter modes to be linear when anisotropy is enabled; the builder disables anisotropy if this constraint is not met to avoid runtime validation errors. - On adapters without `ANISOTROPIC_FILTERING` downlevel support, anisotropy is forced to `1` (disabled). - wgpu 28 does not expose a per-device `Limits::max_sampler_anisotropy`; this change clamps to `[1, 16]` and gates on downlevel capability.
2 parents 43c91a7 + 1d65643 commit ab6f5d9

2 files changed

Lines changed: 146 additions & 4 deletions

File tree

crates/lambda-rs-platform/src/wgpu/texture.rs

Lines changed: 133 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@ pub struct SamplerBuilder {
528528
address_w: AddressMode,
529529
lod_min: f32,
530530
lod_max: f32,
531+
anisotropy_clamp: u16,
531532
}
532533

533534
impl Default for SamplerBuilder {
@@ -549,6 +550,7 @@ impl SamplerBuilder {
549550
address_w: AddressMode::ClampToEdge,
550551
lod_min: 0.0,
551552
lod_max: 32.0,
553+
anisotropy_clamp: 1,
552554
};
553555
}
554556

@@ -621,7 +623,63 @@ impl SamplerBuilder {
621623
return self;
622624
}
623625

624-
fn to_descriptor(&self) -> wgpu::SamplerDescriptor<'_> {
626+
/// Set the maximum anisotropic filtering level.
627+
///
628+
/// Valid values are `1` (disabled) through `16`. Values outside this range
629+
/// are clamped. Higher values improve texture quality at oblique viewing
630+
/// angles but increase GPU cost.
631+
///
632+
/// Common values:
633+
/// - `1`: Disabled (default)
634+
/// - `4`: Good balance of quality and performance
635+
/// - `8`: High quality
636+
/// - `16`: Maximum quality
637+
///
638+
/// Note: Anisotropic filtering is most effective with linear filtering and
639+
/// mipmapped textures. wgpu also requires all filter modes to be linear when
640+
/// anisotropy is enabled; otherwise anisotropy is disabled.
641+
///
642+
/// ```no_run
643+
/// # use lambda_platform::wgpu::texture::{FilterMode, SamplerBuilder};
644+
/// # fn demo(gpu: &lambda_platform::wgpu::gpu::Gpu) {
645+
/// // High-quality sampler for floor/wall textures viewed at angles
646+
/// let aniso_sampler = SamplerBuilder::new()
647+
/// .linear_clamp()
648+
/// .with_mip_filter(FilterMode::Linear)
649+
/// .with_anisotropy_clamp(8)
650+
/// .build(gpu);
651+
///
652+
/// // Default sampler (no anisotropy) for UI textures
653+
/// let ui_sampler = SamplerBuilder::new().linear_clamp().build(gpu);
654+
/// # let _ = (aniso_sampler, ui_sampler);
655+
/// # }
656+
/// ```
657+
pub fn with_anisotropy_clamp(mut self, clamp: u16) -> Self {
658+
self.anisotropy_clamp = clamp.clamp(1, 16);
659+
return self;
660+
}
661+
662+
fn to_descriptor(
663+
&self,
664+
max_supported_anisotropy: u16,
665+
) -> wgpu::SamplerDescriptor<'_> {
666+
let max_supported_anisotropy = max_supported_anisotropy.clamp(1, 16);
667+
let mut anisotropy_clamp =
668+
self.anisotropy_clamp.min(max_supported_anisotropy);
669+
if anisotropy_clamp > 1
670+
&& !matches!(
671+
(self.min_filter, self.mag_filter, self.mipmap_filter),
672+
(FilterMode::Linear, FilterMode::Linear, FilterMode::Linear)
673+
)
674+
{
675+
logging::warn!(
676+
"Sampler anisotropy requested ({}), but all filters must be \
677+
linear; anisotropy disabled.",
678+
anisotropy_clamp
679+
);
680+
anisotropy_clamp = 1;
681+
}
682+
625683
return wgpu::SamplerDescriptor {
626684
label: self.label.as_deref(),
627685
address_mode_u: self.address_u.to_wgpu(),
@@ -632,13 +690,28 @@ impl SamplerBuilder {
632690
mipmap_filter: self.mipmap_filter.to_wgpu_mipmap(),
633691
lod_min_clamp: self.lod_min,
634692
lod_max_clamp: self.lod_max,
693+
anisotropy_clamp,
635694
..Default::default()
636695
};
637696
}
638697

639698
/// Create the sampler on the provided device.
640699
pub fn build(self, gpu: &Gpu) -> Sampler {
641-
let desc = self.to_descriptor();
700+
let requested_anisotropy = self.anisotropy_clamp.clamp(1, 16);
701+
let downlevel = gpu.adapter().get_downlevel_capabilities();
702+
let supports_anisotropy = downlevel
703+
.flags
704+
.contains(wgpu::DownlevelFlags::ANISOTROPIC_FILTERING);
705+
if requested_anisotropy > 1 && !supports_anisotropy {
706+
logging::warn!(
707+
"Sampler anisotropy requested ({}), but adapter does not report \
708+
anisotropic filtering support; anisotropy disabled.",
709+
requested_anisotropy
710+
);
711+
}
712+
713+
let max_supported_anisotropy = if supports_anisotropy { 16 } else { 1 };
714+
let desc = self.to_descriptor(max_supported_anisotropy);
642715
let raw = gpu.device().create_sampler(&desc);
643716
return Sampler {
644717
raw,
@@ -1051,7 +1124,7 @@ mod tests {
10511124
#[test]
10521125
fn sampler_builder_defaults_map() {
10531126
let b = SamplerBuilder::new();
1054-
let d = b.to_descriptor();
1127+
let d = b.to_descriptor(16);
10551128
assert_eq!(d.address_mode_u, wgpu::AddressMode::ClampToEdge);
10561129
assert_eq!(d.address_mode_v, wgpu::AddressMode::ClampToEdge);
10571130
assert_eq!(d.address_mode_w, wgpu::AddressMode::ClampToEdge);
@@ -1060,19 +1133,75 @@ mod tests {
10601133
assert_eq!(d.mipmap_filter, wgpu::MipmapFilterMode::Nearest);
10611134
assert_eq!(d.lod_min_clamp, 0.0);
10621135
assert_eq!(d.lod_max_clamp, 32.0);
1136+
assert_eq!(d.anisotropy_clamp, 1);
10631137
}
10641138

10651139
#[test]
10661140
fn sampler_builder_linear_clamp_map() {
10671141
let b = SamplerBuilder::new()
10681142
.linear_clamp()
10691143
.with_mip_filter(FilterMode::Linear);
1070-
let d = b.to_descriptor();
1144+
let d = b.to_descriptor(16);
10711145
assert_eq!(d.address_mode_u, wgpu::AddressMode::ClampToEdge);
10721146
assert_eq!(d.address_mode_v, wgpu::AddressMode::ClampToEdge);
10731147
assert_eq!(d.address_mode_w, wgpu::AddressMode::ClampToEdge);
10741148
assert_eq!(d.mag_filter, wgpu::FilterMode::Linear);
10751149
assert_eq!(d.min_filter, wgpu::FilterMode::Linear);
10761150
assert_eq!(d.mipmap_filter, wgpu::MipmapFilterMode::Linear);
10771151
}
1152+
1153+
#[test]
1154+
fn sampler_builder_anisotropy_is_clamped_and_passed_through() {
1155+
let b = SamplerBuilder::new()
1156+
.linear_clamp()
1157+
.with_mip_filter(FilterMode::Linear)
1158+
.with_anisotropy_clamp(8);
1159+
assert_eq!(b.to_descriptor(16).anisotropy_clamp, 8);
1160+
assert_eq!(b.to_descriptor(4).anisotropy_clamp, 4);
1161+
assert_eq!(b.to_descriptor(1).anisotropy_clamp, 1);
1162+
}
1163+
1164+
#[test]
1165+
fn sampler_builder_anisotropy_clamps_to_valid_range() {
1166+
assert_eq!(
1167+
SamplerBuilder::new()
1168+
.linear_clamp()
1169+
.with_mip_filter(FilterMode::Linear)
1170+
.with_anisotropy_clamp(0)
1171+
.to_descriptor(16)
1172+
.anisotropy_clamp,
1173+
1
1174+
);
1175+
assert_eq!(
1176+
SamplerBuilder::new()
1177+
.linear_clamp()
1178+
.with_mip_filter(FilterMode::Linear)
1179+
.with_anisotropy_clamp(100)
1180+
.to_descriptor(16)
1181+
.anisotropy_clamp,
1182+
16
1183+
);
1184+
}
1185+
1186+
#[test]
1187+
fn sampler_builder_anisotropy_is_disabled_when_filters_not_all_linear() {
1188+
// Default builder uses nearest filters, so anisotropy must be disabled.
1189+
assert_eq!(
1190+
SamplerBuilder::new()
1191+
.with_anisotropy_clamp(8)
1192+
.to_descriptor(16)
1193+
.anisotropy_clamp,
1194+
1
1195+
);
1196+
1197+
// If mipmap filtering isn't linear, anisotropy must be disabled.
1198+
assert_eq!(
1199+
SamplerBuilder::new()
1200+
.linear()
1201+
.with_anisotropy_clamp(8)
1202+
.to_descriptor(16)
1203+
.anisotropy_clamp,
1204+
1
1205+
);
1206+
}
10781207
}

crates/lambda-rs/src/render/texture.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,19 @@ impl SamplerBuilder {
627627
return self;
628628
}
629629

630+
/// Set the maximum anisotropic filtering level.
631+
///
632+
/// Valid values are `1` (disabled) through `16`. Values outside this range
633+
/// are clamped. Higher values improve texture quality at oblique viewing
634+
/// angles but increase GPU cost.
635+
///
636+
/// Note: Anisotropic filtering is most effective with linear filtering and
637+
/// mipmapped textures.
638+
pub fn with_anisotropy_clamp(mut self, clamp: u16) -> Self {
639+
self.inner = self.inner.with_anisotropy_clamp(clamp);
640+
return self;
641+
}
642+
630643
/// Attach a debug label.
631644
pub fn with_label(mut self, label: &str) -> Self {
632645
self.inner = self.inner.with_label(label);

0 commit comments

Comments
 (0)