render: add opaque CPU sampling and bounded mip chains (#1459)
Co-Authored-By: selune <0xlillith@users.noreply.github.com> Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
7 files changed
+612
No files matched your search
@@ -0,0 +1,191 @@
|
||||
//! Bounded linear area-filtered mip preparation for opaque CPU reference sampling.
|
||||
//! Call from the existing preparation host; row cancellation does not yield a browser event loop.
|
||||
use super::{SamplingError, TextureView};
|
||||
|
||||
/// Derived pixel storage only, excluding the borrowed base and small level metadata.
|
||||
pub const MAX_MIP_TAIL_BYTES: usize = 32 * 1024 * 1024;
|
||||
/// Include this recipe in preparation/cache identity, alongside source/role/policies.
|
||||
/// V1 means floor-halved linear area filtering, opaque RGB and generated alpha=1.
|
||||
pub const MIP_RECIPE_ID: &str = "solidcraft.opaque-linear-area-mips/1";
|
||||
const MAX_LEVELS: usize = 12;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum MipError {
|
||||
Budget,
|
||||
Allocation,
|
||||
Cancelled,
|
||||
Lod,
|
||||
Sampling(SamplingError),
|
||||
}
|
||||
impl std::fmt::Display for MipError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Budget => f.write_str("mip tail storage exceeds the admitted budget"),
|
||||
Self::Allocation => f.write_str("cannot allocate mip tail storage"),
|
||||
Self::Cancelled => f.write_str("mip preparation cancelled"),
|
||||
Self::Lod => f.write_str("LOD must be finite and within the prepared level range"),
|
||||
Self::Sampling(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::error::Error for MipError {}
|
||||
impl From<SamplingError> for MipError {
|
||||
fn from(value: SamplingError) -> Self {
|
||||
Self::Sampling(value)
|
||||
}
|
||||
}
|
||||
|
||||
struct Level {
|
||||
rgba: Vec<f32>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
}
|
||||
impl Level {
|
||||
fn view(&self) -> TextureView<'_> {
|
||||
// Private levels are created only from admitted pixels and convex averages.
|
||||
// Reuse validation rather than rescan every stored pixel on each sample.
|
||||
TextureView { rgba: &self.rgba, width: self.width, height: self.height }
|
||||
}
|
||||
}
|
||||
|
||||
/// Immutable derived levels; level zero borrows the admitted base without copying it.
|
||||
/// Generated levels have alpha=1, matching the explicitly opaque RGB sampler.
|
||||
pub struct MipChain<'a> {
|
||||
base: TextureView<'a>,
|
||||
tail: Vec<Level>,
|
||||
tail_bytes: usize,
|
||||
retained_bytes: usize,
|
||||
}
|
||||
impl<'a> MipChain<'a> {
|
||||
/// Generate all floor-halved levels through 1x1, or return no chain on failure.
|
||||
/// `max_tail_bytes` bounds derived pixel capacity, not total process memory.
|
||||
/// Preparation is synchronous and must run in the caller-owned preparation host.
|
||||
pub fn prepare(base: TextureView<'a>, max_tail_bytes: usize, cancelled: impl Fn() -> bool) -> Result<Self, MipError> {
|
||||
if max_tail_bytes > MAX_MIP_TAIL_BYTES {
|
||||
return Err(MipError::Budget);
|
||||
}
|
||||
check_cancel(&cancelled)?;
|
||||
let required = required_bytes(base.width, base.height)?;
|
||||
if required > max_tail_bytes {
|
||||
return Err(MipError::Budget);
|
||||
}
|
||||
let mut chain = Self { base, tail: Vec::new(), tail_bytes: 0, retained_bytes: size_of::<Self>() };
|
||||
chain.tail.try_reserve_exact(MAX_LEVELS - 1).map_err(|_| MipError::Allocation)?;
|
||||
let metadata_bytes =
|
||||
chain.tail.capacity().checked_mul(size_of::<Level>()).and_then(|n| n.checked_add(size_of::<Self>())).ok_or(MipError::Budget)?;
|
||||
chain.retained_bytes = metadata_bytes;
|
||||
while chain.level_count() < MAX_LEVELS {
|
||||
check_cancel(&cancelled)?;
|
||||
let source = chain.level(chain.level_count() - 1).ok_or(MipError::Budget)?;
|
||||
if source.width == 1 && source.height == 1 {
|
||||
break;
|
||||
}
|
||||
let width = (source.width / 2).max(1);
|
||||
let height = (source.height / 2).max(1);
|
||||
let length = width.checked_mul(height).and_then(|n| n.checked_mul(4)).ok_or(MipError::Budget)?;
|
||||
let mut rgba = Vec::new();
|
||||
rgba.try_reserve_exact(length).map_err(|_| MipError::Allocation)?;
|
||||
let bytes = rgba.capacity().checked_mul(size_of::<f32>()).ok_or(MipError::Budget)?;
|
||||
let next_bytes = chain.tail_bytes.checked_add(bytes).ok_or(MipError::Budget)?;
|
||||
if next_bytes > max_tail_bytes {
|
||||
return Err(MipError::Budget);
|
||||
}
|
||||
let retained_bytes = metadata_bytes.checked_add(next_bytes).ok_or(MipError::Budget)?;
|
||||
for y in 0..height {
|
||||
check_cancel(&cancelled)?;
|
||||
for x in 0..width {
|
||||
let rgb = area_average(source, x, y, width, height)?;
|
||||
rgba.extend(rgb.map(|v| v as f32));
|
||||
rgba.push(1.0);
|
||||
}
|
||||
}
|
||||
check_cancel(&cancelled)?;
|
||||
chain.tail.push(Level { rgba, width, height });
|
||||
chain.tail_bytes = next_bytes;
|
||||
chain.retained_bytes = retained_bytes;
|
||||
}
|
||||
check_cancel(&cancelled)?;
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
pub fn level_count(&self) -> usize {
|
||||
self.tail.len() + 1
|
||||
}
|
||||
pub fn tail_bytes(&self) -> usize {
|
||||
self.tail_bytes
|
||||
}
|
||||
/// Retained pixel/level Vec capacities plus the chain value. Charge to the
|
||||
/// existing host pool; borrowed base ownership and allocator overhead are separate.
|
||||
pub fn retained_bytes(&self) -> usize {
|
||||
self.retained_bytes
|
||||
}
|
||||
/// Borrow a prepared level without allocation or another full-image admission scan.
|
||||
pub fn level(&self, index: usize) -> Option<TextureView<'_>> {
|
||||
if index == 0 { Some(self.base) } else { self.tail.get(index - 1).map(Level::view) }
|
||||
}
|
||||
pub fn dimensions(&self, index: usize) -> Option<[u32; 2]> {
|
||||
self.level(index).map(|v| [v.width as u32, v.height as u32])
|
||||
}
|
||||
|
||||
/// Explicit LOD only: bilinear within each level, linear between adjacent levels.
|
||||
/// This does not choose a footprint, provide anisotropy or certify GPU parity.
|
||||
/// Out-of-range LOD refuses rather than silently clamping to a different policy.
|
||||
pub fn sample_repeat_lod_opaque(&self, uv: [f64; 2], lod: f64) -> Result<[f64; 3], MipError> {
|
||||
let last = self.level_count() - 1;
|
||||
if !lod.is_finite() || lod < 0.0 || lod > last as f64 {
|
||||
return Err(MipError::Lod);
|
||||
}
|
||||
let low = lod.floor() as usize;
|
||||
let high = (low + 1).min(last);
|
||||
let t = lod - low as f64;
|
||||
let a = self.level(low).ok_or(MipError::Lod)?.sample_repeat_opaque(uv)?;
|
||||
let b = self.level(high).ok_or(MipError::Lod)?.sample_repeat_opaque(uv)?;
|
||||
let mut rgb = [0.0; 3];
|
||||
for ((output, a), b) in rgb.iter_mut().zip(a).zip(b) {
|
||||
*output = a * (1.0 - t) + b * t;
|
||||
}
|
||||
Ok(rgb)
|
||||
}
|
||||
}
|
||||
|
||||
fn check_cancel(cancelled: &impl Fn() -> bool) -> Result<(), MipError> {
|
||||
if cancelled() { Err(MipError::Cancelled) } else { Ok(()) }
|
||||
}
|
||||
|
||||
fn required_bytes(mut width: usize, mut height: usize) -> Result<usize, MipError> {
|
||||
let mut bytes = 0usize;
|
||||
for _ in 1..MAX_LEVELS {
|
||||
if width == 1 && height == 1 {
|
||||
return Ok(bytes);
|
||||
}
|
||||
width = (width / 2).max(1);
|
||||
height = (height / 2).max(1);
|
||||
let level_bytes = width.checked_mul(height).and_then(|n| n.checked_mul(4 * size_of::<f32>())).ok_or(MipError::Budget)?;
|
||||
bytes = bytes.checked_add(level_bytes).ok_or(MipError::Budget)?;
|
||||
}
|
||||
if width == 1 && height == 1 { Ok(bytes) } else { Err(MipError::Budget) }
|
||||
}
|
||||
|
||||
fn area_average(source: TextureView<'_>, x: usize, y: usize, width: usize, height: usize) -> Result<[f64; 3], MipError> {
|
||||
// Integer overlap numerators avoid dropping the final row/column at odd sizes.
|
||||
// All factors are bounded by the validated 2048 edge; at most 3x3 source cells.
|
||||
let x_start = x * source.width;
|
||||
let x_end = (x + 1) * source.width;
|
||||
let y_start = y * source.height;
|
||||
let y_end = (y + 1) * source.height;
|
||||
let mut rgb = [0.0; 3];
|
||||
for sy in y_start / height..y_end.div_ceil(height) {
|
||||
let wy = ((sy + 1) * height).min(y_end) - (sy * height).max(y_start);
|
||||
for sx in x_start / width..x_end.div_ceil(width) {
|
||||
let wx = ((sx + 1) * width).min(x_end) - (sx * width).max(x_start);
|
||||
let weight = wx as f64 * wy as f64 / (source.width as f64 * source.height as f64);
|
||||
for (channel, output) in rgb.iter_mut().enumerate() {
|
||||
*output += source.channel(sx, sy, channel)? * weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(rgb)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,115 @@
|
||||
use super::*;
|
||||
use std::cell::Cell;
|
||||
|
||||
fn close(actual: [f64; 3], expected: [f64; 3]) {
|
||||
for (a, b) in actual.into_iter().zip(expected) {
|
||||
assert!((a - b).abs() < 1e-7, "{actual:?} != {expected:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_checkerboard_mean_and_fractional_lod() {
|
||||
let pixels = [0., 0., 0., 0., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0.];
|
||||
let base = TextureView::new(&pixels, [2, 2]).unwrap();
|
||||
let chain = MipChain::prepare(base, 16, || false).unwrap();
|
||||
assert_eq!(chain.level_count(), 2);
|
||||
assert_eq!(chain.tail_bytes(), 16);
|
||||
assert_eq!(chain.retained_bytes(), chain.tail_bytes() + chain.tail.capacity() * size_of::<Level>() + size_of::<MipChain<'_>>());
|
||||
assert_eq!(MIP_RECIPE_ID, "solidcraft.opaque-linear-area-mips/1");
|
||||
close(chain.sample_repeat_lod_opaque([0.25, 0.25], 0.).unwrap(), [0.; 3]);
|
||||
close(chain.sample_repeat_lod_opaque([0.25, 0.25], 1.).unwrap(), [0.5; 3]);
|
||||
close(chain.sample_repeat_lod_opaque([0.25, 0.25], 0.25).unwrap(), [0.125; 3]);
|
||||
close(chain.sample_repeat_lod_opaque([0.25, 0.25], 0.5).unwrap(), [0.25; 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_edges_contribute_by_area_instead_of_being_dropped_or_overweighted() {
|
||||
// Five pixels reduce to two equal-width cells: [0,0,0] and [0,0,1].
|
||||
// The middle pixel straddles the boundary, the last pixel weighs 1/2.5.
|
||||
let mut pixels = [0., 0., 0., 1.].repeat(5);
|
||||
pixels[16] = 1.;
|
||||
let chain = MipChain::prepare(TextureView::new(&pixels, [5, 1]).unwrap(), 48, || false).unwrap();
|
||||
assert_eq!(chain.dimensions(1), Some([2, 1]));
|
||||
close(chain.level(1).unwrap().sample_repeat_opaque([0.75, 0.5]).unwrap(), [0.4, 0., 0.]);
|
||||
close(chain.sample_repeat_lod_opaque([0., 0.], 2.).unwrap(), [0.2, 0., 0.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rectangular_odd_pyramid_preserves_independent_mean() {
|
||||
let mut pixels = Vec::new();
|
||||
for i in 0..15 {
|
||||
pixels.extend([i as f32 / 16., 0.25, (14 - i) as f32 / 16., (i % 2) as f32]);
|
||||
}
|
||||
let chain = MipChain::prepare(TextureView::new(&pixels, [5, 3]).unwrap(), 48, || false).unwrap();
|
||||
assert_eq!(chain.level_count(), 3);
|
||||
assert_eq!(chain.dimensions(0), Some([5, 3]));
|
||||
assert_eq!(chain.dimensions(1), Some([2, 1]));
|
||||
assert_eq!(chain.dimensions(2), Some([1, 1]));
|
||||
assert_eq!(chain.dimensions(usize::MAX), None);
|
||||
close(chain.sample_repeat_lod_opaque([-9.5, 12.], 2.).unwrap(), [7. / 16., 0.25, 7. / 16.]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_texel_borrows_base_and_needs_no_pixel_budget() {
|
||||
let pixels = [0.25, 0.5, 0.75, 0.];
|
||||
let chain = MipChain::prepare(TextureView::new(&pixels, [1, 1]).unwrap(), 0, || false).unwrap();
|
||||
assert_eq!(chain.level_count(), 1);
|
||||
assert_eq!(chain.tail_bytes(), 0);
|
||||
assert!(std::ptr::eq(chain.level(0).unwrap().rgba.as_ptr(), pixels.as_ptr()));
|
||||
close(chain.sample_repeat_lod_opaque([-0.1, 0.7], 0.).unwrap(), [0.25, 0.5, 0.75]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_images_and_single_axes_are_invariant_at_all_lods() {
|
||||
for dimensions in [[1, 7], [7, 1], [7, 5]] {
|
||||
let pixels = [0.25, 0.5, 0.75, 0.].repeat((dimensions[0] * dimensions[1]) as usize);
|
||||
let chain = MipChain::prepare(TextureView::new(&pixels, dimensions).unwrap(), MAX_MIP_TAIL_BYTES, || false).unwrap();
|
||||
for quarter in 0..=(chain.level_count() - 1) * 4 {
|
||||
close(chain.sample_repeat_lod_opaque([-2.34, 1.92], quarter as f64 / 4.).unwrap(), [0.25, 0.5, 0.75]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_lod_and_uv_refuse_without_changing_the_chain() {
|
||||
let pixels = [0., 0., 0., 1.].repeat(4);
|
||||
let chain = MipChain::prepare(TextureView::new(&pixels, [2, 2]).unwrap(), 16, || false).unwrap();
|
||||
for lod in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, -0.001, 1.001, f64::MAX] {
|
||||
assert_eq!(chain.sample_repeat_lod_opaque([0., 0.], lod), Err(MipError::Lod));
|
||||
}
|
||||
assert_eq!(chain.sample_repeat_lod_opaque([f64::NAN, 0.], 0.5), Err(MipError::Sampling(SamplingError::Coordinates)));
|
||||
close(chain.sample_repeat_lod_opaque([0.25, 0.25], 0.).unwrap(), [0.; 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_budget_and_global_cap_are_admitted_before_work() {
|
||||
let pixels = [0., 0., 0., 1.].repeat(4);
|
||||
let base = TextureView::new(&pixels, [2, 2]).unwrap();
|
||||
assert!(matches!(MipChain::prepare(base, 15, || false), Err(MipError::Budget)));
|
||||
assert!(MipChain::prepare(base, 16, || false).is_ok());
|
||||
assert!(matches!(MipChain::prepare(base, MAX_MIP_TAIL_BYTES + 1, || false), Err(MipError::Budget)));
|
||||
assert!(matches!(MipChain::prepare(base, usize::MAX, || false), Err(MipError::Budget)));
|
||||
assert_eq!(required_bytes(2048, 2048).unwrap(), 22_369_616);
|
||||
assert!(required_bytes(2048, 2048).unwrap() < MAX_MIP_TAIL_BYTES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancellation_before_during_and_after_preparation_never_returns_a_partial_chain() {
|
||||
let pixels = [0.25, 0.5, 0.75, 0.].repeat(64);
|
||||
let base = TextureView::new(&pixels, [8, 8]).unwrap();
|
||||
let successful_calls = Cell::new(0);
|
||||
MipChain::prepare(base, MAX_MIP_TAIL_BYTES, || {
|
||||
successful_calls.set(successful_calls.get() + 1);
|
||||
false
|
||||
})
|
||||
.unwrap();
|
||||
for stop in 1..=successful_calls.get() {
|
||||
let calls = Cell::new(0);
|
||||
let result = MipChain::prepare(base, MAX_MIP_TAIL_BYTES, || {
|
||||
calls.set(calls.get() + 1);
|
||||
calls.get() == stop
|
||||
});
|
||||
assert!(matches!(result, Err(MipError::Cancelled)));
|
||||
}
|
||||
assert!(MipChain::prepare(base, MAX_MIP_TAIL_BYTES, || false).is_ok());
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use super::*;
|
||||
|
||||
const PIXELS: [f32; 16] = [1., 0., 0., 0., 0., 1., 0., 1., 0., 0., 1., 1., 1., 1., 1., 1.];
|
||||
|
||||
fn close(actual: [f64; 3], expected: [f64; 3]) {
|
||||
for (a, b) in actual.into_iter().zip(expected) {
|
||||
assert!((a - b).abs() < 1e-9, "{actual:?} != {expected:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn centers_orientation_and_explicit_opaque_alpha() {
|
||||
let view = TextureView::new(&PIXELS, [2, 2]).unwrap();
|
||||
for (uv, rgb) in [([0.25, 0.25], [1., 0., 0.]), ([0.75, 0.25], [0., 1., 0.]), ([0.25, 0.75], [0., 0., 1.]), ([0.75, 0.75], [1., 1., 1.])] {
|
||||
close(view.sample_repeat_opaque(uv).unwrap(), rgb);
|
||||
}
|
||||
close(view.sample_repeat_opaque([0.5, 0.5]).unwrap(), [0.5; 3]);
|
||||
close(view.sample_repeat_opaque([0.5, 0.25]).unwrap(), [0.5, 0.5, 0.]);
|
||||
close(view.sample_repeat_opaque([0.375, 0.625]).unwrap(), [0.375, 0.25, 0.75]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linear_black_white_mixing_is_not_encoded_srgb_averaging() {
|
||||
let pixels = [0., 0., 0., 1., 1., 1., 1., 1.];
|
||||
let view = TextureView::new(&pixels, [2, 1]).unwrap();
|
||||
// Linear 0.5, which would encode to about sRGB 0.735, not encoded 0.5.
|
||||
close(view.sample_repeat_opaque([0.5, 0.5]).unwrap(), [0.5; 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_repeats_and_both_periodic_seams() {
|
||||
let view = TextureView::new(&PIXELS, [2, 2]).unwrap();
|
||||
for uv in [[0., 0.], [1., 1.], [-1., -1.], [MAX_REPEAT_COORDINATE, -MAX_REPEAT_COORDINATE]] {
|
||||
close(view.sample_repeat_opaque(uv).unwrap(), [0.5; 3]);
|
||||
}
|
||||
close(view.sample_repeat_opaque([-0.75, -0.25]).unwrap(), [0., 0., 1.]);
|
||||
for uv in [[0.13, 0.92], [0., 0.25], [0.75, 0.]] {
|
||||
close(view.sample_repeat_opaque(uv).unwrap(), view.sample_repeat_opaque([uv[0] - 7., uv[1] + 9.]).unwrap());
|
||||
}
|
||||
for epsilon in [-1e-12, 0., 1e-12] {
|
||||
close(view.sample_repeat_opaque([epsilon, 0.25]).unwrap(), [0.5, 0.5, 0.]);
|
||||
close(view.sample_repeat_opaque([0.25, epsilon]).unwrap(), [0.5, 0., 0.5]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_and_single_texel_invariance() {
|
||||
for dimensions in [[1, 1], [1, 3], [3, 1], [3, 5]] {
|
||||
let pixels = [0.25, 0.5, 0.75, 0.].repeat((dimensions[0] * dimensions[1]) as usize);
|
||||
let view = TextureView::new(&pixels, dimensions).unwrap();
|
||||
for uv in [[0., 0.], [-12.125, 3.25], [1., 1.], [MAX_REPEAT_COORDINATE - 0.125, 0.5]] {
|
||||
close(view.sample_repeat_opaque(uv).unwrap(), [0.25, 0.5, 0.75]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_hostile_storage_and_channels() {
|
||||
for dimensions in [[0, 1], [1, 0], [MAX_TEXTURE_EDGE + 1, 1], [u32::MAX, u32::MAX]] {
|
||||
assert_eq!(TextureView::new(&[], dimensions).unwrap_err(), SamplingError::Dimensions);
|
||||
}
|
||||
for pixels in [&[][..], &[0.; 3][..], &[0.; 5][..]] {
|
||||
assert_eq!(TextureView::new(pixels, [1, 1]).unwrap_err(), SamplingError::Length);
|
||||
}
|
||||
for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -0.01, 1.01] {
|
||||
for channel in 0..4 {
|
||||
let mut pixels = [0.; 4];
|
||||
pixels[channel] = invalid;
|
||||
assert_eq!(TextureView::new(&pixels, [1, 1]).unwrap_err(), SamplingError::Values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_nonfinite_and_extreme_coordinates() {
|
||||
let view = TextureView::new(&PIXELS, [2, 2]).unwrap();
|
||||
for invalid in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY, f64::MAX, MAX_REPEAT_COORDINATE + 0.001, -MAX_REPEAT_COORDINATE - 0.001] {
|
||||
for uv in [[invalid, 0.], [0., invalid]] {
|
||||
assert_eq!(view.sample_repeat_opaque(uv), Err(SamplingError::Coordinates));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_bound_retains_fractional_centers_and_maximum_edge() {
|
||||
let view = TextureView::new(&PIXELS, [2, 2]).unwrap();
|
||||
close(view.sample_repeat_opaque([MAX_REPEAT_COORDINATE - 0.75, -MAX_REPEAT_COORDINATE + 0.25]).unwrap(), [1., 0., 0.]);
|
||||
let mut pixels = [0., 0., 0., 1.].repeat(MAX_TEXTURE_EDGE as usize);
|
||||
pixels[0] = 1.;
|
||||
let view = TextureView::new(&pixels, [MAX_TEXTURE_EDGE, 1]).unwrap();
|
||||
let half_texel = 0.5 / f64::from(MAX_TEXTURE_EDGE);
|
||||
close(view.sample_repeat_opaque([half_texel, 0.]).unwrap(), [1., 0., 0.]);
|
||||
close(view.sample_repeat_opaque([half_texel - MAX_REPEAT_COORDINATE, 0.]).unwrap(), [1., 0., 0.]);
|
||||
}
|
||||
Reference in new issue
Block a user