| // Copyright 2025 the Vello Authors |
| // SPDX-License-Identifier: Apache-2.0 OR MIT |
| |
| //! Rendering strips. |
| use crate::flatten::{Line, Point}; |
| use crate::peniko::Fill; |
| use crate::tile::{Tile, Tiles}; |
| use alloc::vec::Vec; |
| use bytemuck::{Pod, Zeroable}; |
| use core::{f32, panic}; |
| use fearless_simd::*; |
| use std::format; |
| use std::ops::{Add, AddAssign, BitAnd, BitOr, BitXor, Div, Mul, Sub, SubAssign}; |
| use std::println; |
| use std::string::String; |
| use std::vec; |
| |
| // TODO make the tile simply hold the extra data |
| |
| /// A strip. |
| #[derive(Debug, Clone, Copy)] |
| pub struct Strip { |
| /// The x coordinate of the strip, in user coordinates. |
| pub x: u16, |
| /// The y coordinate of the strip, in user coordinates. |
| pub y: u16, |
| /// Packed alpha index and fill gap flag. |
| /// |
| /// Bit layout (u32): |
| /// - bit 31: `fill_gap` (See `Strip::fill_gap()`). |
| /// - bits 0..=30: `alpha_idx` (See `Strip::alpha_idx()`). |
| packed_alpha_idx_fill_gap: u32, |
| } |
| |
| impl Strip { |
| /// The bit mask for `fill_gap` packed into `packed_alpha_idx_fill_gap`. |
| const FILL_GAP_MASK: u32 = 1 << 31; |
| |
| /// Creates a new strip. |
| pub fn new(x: u16, y: u16, alpha_idx: u32, fill_gap: bool) -> Self { |
| // Ensure `alpha_idx` does not collide with the fill flag bit. |
| assert!( |
| alpha_idx & Self::FILL_GAP_MASK == 0, |
| "`alpha_idx` too large" |
| ); |
| let fill_gap = u32::from(fill_gap) << 31; |
| Self { |
| x, |
| y, |
| packed_alpha_idx_fill_gap: alpha_idx | fill_gap, |
| } |
| } |
| |
| pub fn is_sentinel(&self) -> bool { |
| self.x == u16::MAX |
| } |
| |
| /// Returns the y coordinate of the strip, in strip units. |
| pub fn strip_y(&self) -> u16 { |
| self.y / Tile::HEIGHT |
| } |
| |
| /// Returns the alpha index. |
| #[inline(always)] |
| pub fn alpha_idx(&self) -> u32 { |
| self.packed_alpha_idx_fill_gap & !Self::FILL_GAP_MASK |
| } |
| |
| /// Sets the alpha index. |
| /// |
| /// Note that the largest value that can be stored in the alpha index is `u32::MAX << 1`, as the |
| /// highest bit is reserved for `fill_gap`. |
| #[inline(always)] |
| pub fn set_alpha_idx(&mut self, alpha_idx: u32) { |
| // Ensure `alpha_idx` does not collide with the fill flag bit. |
| assert!( |
| alpha_idx & Self::FILL_GAP_MASK == 0, |
| "`alpha_idx` too large" |
| ); |
| let fill_gap = self.packed_alpha_idx_fill_gap & Self::FILL_GAP_MASK; |
| self.packed_alpha_idx_fill_gap = alpha_idx | fill_gap; |
| } |
| |
| /// Returns whether the gap that lies between this strip and the previous in the same row should be filled. |
| #[inline(always)] |
| pub fn fill_gap(&self) -> bool { |
| (self.packed_alpha_idx_fill_gap & Self::FILL_GAP_MASK) != 0 |
| } |
| |
| /// Sets whether the gap that lies between this strip and the previous in the same row should be filled. |
| #[inline(always)] |
| pub fn set_fill_gap(&mut self, fill: bool) { |
| let fill = u32::from(fill) << 31; |
| self.packed_alpha_idx_fill_gap = |
| (self.packed_alpha_idx_fill_gap & !Self::FILL_GAP_MASK) | fill; |
| } |
| } |
| |
| impl PartialEq for Strip { |
| fn eq(&self, other: &Self) -> bool { |
| self.x == other.x && |
| self.y == other.y && |
| self.packed_alpha_idx_fill_gap == other.packed_alpha_idx_fill_gap |
| } |
| } |
| |
| impl Eq for Strip {} |
| |
| // Intersection Layout (6 bits) |
| // MSB LSB |
| // | 5 | 4 | 3 | 2 | 1 | 0 | |
| // | W | P | R | L | B | T | |
| pub const INTERSECTS_TOP_MASK: u32 = 1 << 0; |
| pub const INTERSECTS_BOTTOM_MASK: u32 = 1 << 1; |
| pub const INTERSECTS_LEFT_MASK: u32 = 1 << 2; |
| pub const INTERSECTS_RIGHT_MASK: u32 = 1 << 3; |
| pub const PERFECT_MASK: u32 = 1 << 4; // Formerly "Ambiguous" or Perfect Corner |
| pub const WINDING_MASK: u32 = 1 << 5; // Winding direction bit |
| |
| // Masks for packed_winding_line_idx |
| const INTERSECTION_DATA_WIDTH: u32 = 6; |
| const INTERSECTION_DATA_MASK: u32 = (1 << INTERSECTION_DATA_WIDTH) - 1; |
| |
| // Masks for packed_scanned_winding |
| const IS_END_TILE_BIT: u32 = 0; |
| const FILL_RULE_BIT: u32 = 1; |
| // Scanned winding moved to bits 8-15 to make room for 9-bit ID at top |
| const SCANNED_WINDING_SHIFT: u32 = 8; |
| const SCANNED_WINDING_MASK: u32 = 0xFF; // i8 |
| // Start ID moved to upper 9 bits (23-31) |
| const TILE_START_ID_SHIFT: u32 = 23; |
| const TILE_START_ID_MASK: u32 = 0x1FF; // 9 bits (0-511) |
| |
| /// Pre-Merge-Tile: |
| #[repr(C)] |
| #[derive(Debug, Clone, Copy, Zeroable, Pod)] |
| pub struct PreMergeTile { |
| /// The index into the alpha buffer that an "end tile" should write to. |
| pub alpha_index: u32, |
| |
| /// Contains the Intersection Data and the Line Buffer Index. |
| /// MSB LSB |
| /// 31------------------------------------------------------6|5----0| |
| /// | Line Buffer Index (26 bits) | Ints | |
| /// |
| /// - **Ints**: Intersection Data (6 bits) |
| pub packed_winding_line_idx: u32, |
| |
| /// Contains the Scanned Winding, Tile Start ID, and Flags. |
| /// MSB LSB |
| /// 31-----------------23|22-------16|15--------8|7-----------2|1|0| |
| /// | Start ID (9b) | Unused | Scan Wind | Unused |F|E| |
| /// |
| /// - **Start ID**: u16 (Tile Start ID, 9 bits) |
| /// - **Scan Wind**: i8 (Exclusive prefix sum) |
| /// - **F**: Fill Rule (bool) |
| /// - **E**: Is End Tile (bool) |
| pub packed_scanned_winding: u32, |
| |
| /// Contains the packed u16 tile x and y coordinates. |
| /// MSB LSB |
| /// 31-------16|15--------0| |
| /// | Tile Y | Tile X | |
| pub packed_xy: u32, |
| |
| pub line: Line, |
| } |
| |
| impl PreMergeTile { |
| pub fn set_tile_xy(&mut self, x: u16, y: u16) { |
| self.packed_xy = ((y as u32) << 16) | (x as u32); |
| } |
| |
| #[inline] |
| pub const fn tile_x(&self) -> u16 { |
| (self.packed_xy & 0xFFFF) as u16 |
| } |
| |
| #[inline] |
| pub const fn tile_y(&self) -> u16 { |
| (self.packed_xy >> 16) as u16 |
| } |
| |
| pub fn pack_winding_and_line(&mut self, line_idx: u32, intersection_data: u32) { |
| self.packed_winding_line_idx = |
| (line_idx << INTERSECTION_DATA_WIDTH) | (intersection_data & INTERSECTION_DATA_MASK); |
| } |
| |
| pub fn set_line_index(&mut self, idx: u32) { |
| let intersection = self.packed_winding_line_idx & INTERSECTION_DATA_MASK; |
| self.packed_winding_line_idx = (idx << INTERSECTION_DATA_WIDTH) | intersection; |
| } |
| |
| #[inline] |
| pub const fn line_index(&self) -> u32 { |
| self.packed_winding_line_idx >> INTERSECTION_DATA_WIDTH |
| } |
| |
| pub fn set_intersection_data(&mut self, data: u32) { |
| let line_idx = self.packed_winding_line_idx & !INTERSECTION_DATA_MASK; |
| self.packed_winding_line_idx = line_idx | (data & INTERSECTION_DATA_MASK); |
| } |
| |
| #[inline] |
| pub const fn intersection_data(&self) -> u32 { |
| self.packed_winding_line_idx & INTERSECTION_DATA_MASK |
| } |
| |
| #[inline] |
| pub const fn intersects_top(&self) -> bool { |
| (self.intersection_data() & INTERSECTS_TOP_MASK) != 0 |
| } |
| |
| #[inline] |
| pub const fn intersects_bottom(&self) -> bool { |
| (self.intersection_data() & INTERSECTS_BOTTOM_MASK) != 0 |
| } |
| |
| #[inline] |
| pub const fn intersects_left(&self) -> bool { |
| (self.intersection_data() & INTERSECTS_LEFT_MASK) != 0 |
| } |
| |
| #[inline] |
| pub const fn intersects_right(&self) -> bool { |
| (self.intersection_data() & INTERSECTS_RIGHT_MASK) != 0 |
| } |
| |
| #[inline] |
| pub const fn is_perfect(&self) -> bool { |
| (self.intersection_data() & PERFECT_MASK) != 0 |
| } |
| |
| #[inline] |
| pub const fn winding_bit(&self) -> bool { |
| (self.intersection_data() & WINDING_MASK) != 0 |
| } |
| |
| pub fn set_is_end_tile(&mut self, is_end: bool) { |
| if is_end { |
| self.packed_scanned_winding |= 1 << IS_END_TILE_BIT; |
| } else { |
| self.packed_scanned_winding &= !(1 << IS_END_TILE_BIT); |
| } |
| } |
| |
| #[inline] |
| pub const fn is_end_tile(&self) -> bool { |
| (self.packed_scanned_winding & (1 << IS_END_TILE_BIT)) != 0 |
| } |
| |
| pub fn set_fill_rule(&mut self, is_non_zero: bool) { |
| if is_non_zero { |
| self.packed_scanned_winding |= 1 << FILL_RULE_BIT; |
| } else { |
| self.packed_scanned_winding &= !(1 << FILL_RULE_BIT); |
| } |
| } |
| |
| #[inline] |
| pub const fn fill_rule(&self) -> bool { |
| (self.packed_scanned_winding & (1 << FILL_RULE_BIT)) != 0 |
| } |
| |
| pub fn set_tile_start_id(&mut self, id: u16) { |
| self.packed_scanned_winding |= ((id as u32) & TILE_START_ID_MASK) << TILE_START_ID_SHIFT; |
| } |
| |
| #[inline] |
| pub const fn tile_start_id(&self) -> u16 { |
| ((self.packed_scanned_winding >> TILE_START_ID_SHIFT) & TILE_START_ID_MASK) as u16 |
| } |
| |
| pub fn set_scanned_winding(&mut self, winding: i8) { |
| // Clear only the scanned winding bits |
| self.packed_scanned_winding |= (winding as u8 as u32) << SCANNED_WINDING_SHIFT; |
| } |
| |
| #[inline] |
| pub const fn scanned_winding(&self) -> u32 { |
| ((self.packed_scanned_winding >> SCANNED_WINDING_SHIFT) & SCANNED_WINDING_MASK) as i8 as u32 |
| } |
| } |
| |
| impl PartialEq for PreMergeTile { |
| fn eq(&self, other: &Self) -> bool { |
| let valid_bits_mask = |
| (1 << IS_END_TILE_BIT) | |
| (1 << FILL_RULE_BIT) | |
| (SCANNED_WINDING_MASK << SCANNED_WINDING_SHIFT) | |
| (TILE_START_ID_MASK << TILE_START_ID_SHIFT); |
| |
| self.alpha_index == other.alpha_index && |
| self.packed_winding_line_idx == other.packed_winding_line_idx && |
| (self.packed_scanned_winding & valid_bits_mask) == (other.packed_scanned_winding & valid_bits_mask) && |
| self.packed_xy == other.packed_xy && |
| self.line == other.line |
| } |
| } |
| |
| impl Eq for PreMergeTile {} |
| |
| const MASK_WIDTH: u32 = 64; |
| const MASK_HEIGHT: u32 = 64; |
| const SAMPLE_COUNT: usize = 8; |
| const PACKING_SCALE: f32 = 0.5; |
| |
| fn algorithm_lut_mask_halfplane(line_p0: [f32; 2], line_p1: [f32; 2], lut: &[u8]) -> u8 { |
| let p0 = line_p0; |
| let p1 = line_p1; |
| |
| let dir = (p1[0] - p0[0], p1[1] - p0[1]); |
| let n_unnormalized = (dir.1, -dir.0); |
| |
| let mut len = n_unnormalized.0.hypot(n_unnormalized.1); |
| if len < 1e-9 { |
| len = 1e-9; |
| } |
| |
| let n = (n_unnormalized.0 / len, n_unnormalized.1 / len); |
| let mut c = n.0 * p0[0] + n.1 * p0[1]; |
| c -= 0.5 * (n.0 + n.1); |
| |
| let c_lookup = c; |
| let sign = if c < 0.0 { -1.0 } else { 1.0 }; |
| |
| let c2 = (1.0 - c_lookup * PACKING_SCALE * sign).max(0.0); |
| |
| let n_rev = (c2 * n.0 * sign, c2 * n.1 * sign); |
| |
| let mut uv = (n_rev.0 * 0.5 + 0.5, n_rev.1 * 0.5 + 0.5); |
| |
| if sign < 0.0 && uv.0 == 0.5 { |
| uv.0 = 0.5f32.next_down(); |
| } |
| |
| let u_f = (uv.0 * MASK_WIDTH as f32).floor(); |
| let v_f = (uv.1 * MASK_HEIGHT as f32).floor(); |
| |
| let mask_width_m1 = (MASK_WIDTH - 1) as i32; |
| let mask_height_m1 = (MASK_HEIGHT - 1) as i32; |
| |
| let u = (u_f as i32).max(0).min(mask_width_m1) as u32; |
| let v = (v_f as i32).max(0).min(mask_height_m1) as u32; |
| |
| let index = (v * MASK_WIDTH + u) as usize; |
| lut[index] |
| } |
| |
| /// Render the tiles stored in `tiles` into the strip and alpha buffer. |
| pub fn render( |
| _level: Level, |
| tiles: &Tiles, |
| strip_buf: &mut Vec<Strip>, |
| pmt_buf: &mut Vec<PreMergeTile>, |
| alpha_buf: &mut Vec<u8>, |
| fill_rule: Fill, |
| lines: &[Line], |
| lut: &Vec<u8>, |
| ) { |
| let start_index = pmt_buf.len(); |
| let fr = Fill::NonZero; |
| //let fr = fill_rule; |
| prepare_gpu_inputs( |
| tiles, |
| lines, |
| strip_buf, |
| pmt_buf, |
| alpha_buf, |
| fr, |
| ); |
| |
| // This enables the CPU side generation. |
| // let lut_u32: &[u32] = bytemuck::cast_slice(&lut); |
| // gpu_non_zero(pmt_buf, alpha_buf, lut_u32, start_index); |
| } |
| |
| // For emulating the behavior of the shader |
| const BLOCK_DIM: u32 = 256; |
| // The number of tiles processed per workgroup, 1 thread per row, 4 rows per tile |
| const PART_SIZE: u32 = BLOCK_DIM / 4; |
| const PART_SIZE_LG: u32 = 6; |
| const INVALID_ID: u32 = 256; |
| const EPSILON: f32 = 1e-6; |
| |
| /// Prepares gpu inputs |
| fn prepare_gpu_inputs( |
| tiles: &Tiles, |
| lines: &[Line], |
| strip_buf: &mut Vec<Strip>, |
| pre_merge_buf: &mut Vec<PreMergeTile>, |
| alpha_buf: &mut Vec<u8>, |
| fill_rule: Fill, |
| ) { |
| if tiles.is_empty() { |
| return; |
| } |
| |
| let should_fill = |winding: i8| match fill_rule { |
| Fill::NonZero => winding != 0, |
| Fill::EvenOdd => winding % 2 != 0, |
| }; |
| |
| let mut winding_delta: i8 = 0; |
| let mut prev_tile = *tiles.get(0); |
| |
| let mut alpha_offset: u32 = 0; |
| let initial_alpha_len = alpha_buf.len() as u32; |
| |
| const SENTINEL: Tile = Tile::new(u16::MAX, u16::MAX, 0, 0); |
| |
| let mut start_tile_idx = 0; |
| let mut strip = Strip::new( |
| prev_tile.x * Tile::WIDTH, |
| prev_tile.y * Tile::HEIGHT, |
| initial_alpha_len, |
| should_fill(winding_delta), |
| ); |
| for (tile_idx, tile) in tiles.iter().copied().chain([SENTINEL]).enumerate() { |
| let is_start_tile = tile_idx == 0 || !prev_tile.same_loc(&tile); |
| let is_start_segment = tile_idx == 0 || (is_start_tile && !prev_tile.prev_loc(&tile)); |
| |
| if is_start_tile { |
| start_tile_idx = pre_merge_buf.len(); |
| if tile_idx > 0 { |
| alpha_offset += (Tile::WIDTH * Tile::HEIGHT) as u32; |
| } |
| } |
| |
| if is_start_segment { |
| strip_buf.push(strip); |
| |
| let is_sentinel = tile_idx == tiles.len() as usize; |
| if !prev_tile.same_row(&tile) { |
| if winding_delta != 0 || is_sentinel { |
| strip_buf.push(Strip::new( |
| u16::MAX, |
| prev_tile.y * Tile::HEIGHT, |
| initial_alpha_len + alpha_offset, |
| should_fill(winding_delta), |
| )); |
| } |
| winding_delta = 0; |
| } |
| |
| if is_sentinel { |
| break; |
| } |
| |
| strip = Strip::new( |
| tile.x * Tile::WIDTH, |
| tile.y * Tile::HEIGHT, |
| initial_alpha_len + alpha_offset, |
| should_fill(winding_delta), |
| ); |
| } |
| |
| // TODO: Try making tile and pmt the same? |
| // TODO: Move line off the pmt |
| let line = lines[tile.line_idx() as usize]; |
| let mut pmt = PreMergeTile { |
| alpha_index: initial_alpha_len + alpha_offset, |
| packed_winding_line_idx: tile.packed_winding_line_idx, |
| packed_scanned_winding: 0, |
| packed_xy: 0, |
| line, |
| }; |
| pmt.set_tile_xy(tile.x, tile.y); |
| pmt.set_scanned_winding(winding_delta); |
| |
| let sign = if line.p1.y >= line.p0.y { 1i8 } else { -1i8 }; |
| |
| let signed_winding = sign * tile.winding() as i8; |
| winding_delta += signed_winding; |
| |
| pmt.set_is_end_tile( |
| tile_idx == tiles.len() as usize - 1 || !tiles.get(tile_idx as u32 + 1).same_loc(&tile), |
| ); |
| |
| let block_idx = pre_merge_buf.len() >> PART_SIZE_LG << PART_SIZE_LG; |
| let start_tile_pack = if start_tile_idx < block_idx { |
| INVALID_ID |
| } else { |
| (start_tile_idx - block_idx) as u32 |
| }; |
| pmt.set_tile_start_id(start_tile_pack as u16); |
| pmt.set_fill_rule(is_start_tile); |
| |
| pre_merge_buf.push(pmt); |
| prev_tile = tile; |
| } |
| |
| alpha_buf.resize(alpha_buf.len() + alpha_offset as usize, 0); |
| } |
| |
| #[derive(Clone, Copy, Debug, PartialEq)] |
| pub struct Vec2f { |
| pub x: f32, |
| pub y: f32, |
| } |
| |
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| pub struct Vec2u { |
| pub x: u32, |
| pub y: u32, |
| } |
| |
| #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| pub struct Vec2bool { |
| pub x: bool, |
| pub y: bool, |
| } |
| |
| fn vec2f(x: f32, y: f32) -> Vec2f { |
| Vec2f { x, y } |
| } |
| fn vec2f_s(s: f32) -> Vec2f { |
| Vec2f { x: s, y: s } |
| } // splat |
| |
| fn vec2u(x: u32, y: u32) -> Vec2u { |
| Vec2u { x, y } |
| } |
| |
| fn vec2bool(val: bool) -> Vec2bool { |
| Vec2bool { x: val, y: val } |
| } |
| |
| impl Vec2f { |
| fn yx(&self) -> Vec2f { |
| Vec2f { |
| x: self.y, |
| y: self.x, |
| } |
| } |
| fn xy(&self) -> Vec2f { |
| *self |
| } |
| } |
| |
| trait Selectable<Cond> { |
| fn select(false_val: Self, true_val: Self, cond: Cond) -> Self; |
| } |
| |
| impl Selectable<bool> for f32 { |
| fn select(f: f32, t: f32, cond: bool) -> f32 { |
| if cond { t } else { f } |
| } |
| } |
| |
| impl Selectable<bool> for Vec2f { |
| fn select(f: Vec2f, t: Vec2f, cond: bool) -> Vec2f { |
| if cond { t } else { f } |
| } |
| } |
| |
| impl Selectable<Vec2bool> for Vec2f { |
| fn select(f: Vec2f, t: Vec2f, cond: Vec2bool) -> Vec2f { |
| Vec2f { |
| x: if cond.x { t.x } else { f.x }, |
| y: if cond.y { t.y } else { f.y }, |
| } |
| } |
| } |
| |
| impl Selectable<Vec2bool> for Vec2u { |
| fn select(f: Vec2u, t: Vec2u, cond: Vec2bool) -> Vec2u { |
| Vec2u { |
| x: if cond.x { t.x } else { f.x }, |
| y: if cond.y { t.y } else { f.y }, |
| } |
| } |
| } |
| |
| fn select<T, C>(f: T, t: T, c: C) -> T |
| where |
| T: Selectable<C>, |
| { |
| T::select(f, t, c) |
| } |
| |
| fn clamp(val: Vec2f, min: Vec2f, max: Vec2f) -> Vec2f { |
| Vec2f { |
| x: val.x.clamp(min.x, max.x), |
| y: val.y.clamp(min.y, max.y), |
| } |
| } |
| |
| fn any(v: Vec2bool) -> bool { |
| v.x || v.y |
| } |
| |
| impl Add for Vec2f { |
| type Output = Self; |
| fn add(self, rhs: Self) -> Self { |
| vec2f(self.x + rhs.x, self.y + rhs.y) |
| } |
| } |
| impl Sub for Vec2f { |
| type Output = Self; |
| fn sub(self, rhs: Self) -> Self { |
| vec2f(self.x - rhs.x, self.y - rhs.y) |
| } |
| } |
| impl Mul for Vec2f { |
| type Output = Self; |
| fn mul(self, rhs: Self) -> Self { |
| vec2f(self.x * rhs.x, self.y * rhs.y) |
| } |
| } |
| impl Mul<f32> for Vec2f { |
| type Output = Self; |
| fn mul(self, rhs: f32) -> Self { |
| vec2f(self.x * rhs, self.y * rhs) |
| } |
| } |
| impl SubAssign for Vec2f { |
| fn sub_assign(&mut self, rhs: Self) { |
| self.x -= rhs.x; |
| self.y -= rhs.y; |
| } |
| } |
| |
| impl Vec2f { |
| fn lt(&self, rhs: Vec2f) -> Vec2bool { |
| Vec2bool { |
| x: self.x < rhs.x, |
| y: self.y < rhs.y, |
| } |
| } |
| fn gt(&self, rhs: Vec2f) -> Vec2bool { |
| Vec2bool { |
| x: self.x > rhs.x, |
| y: self.y > rhs.y, |
| } |
| } |
| } |
| |
| impl AddAssign for Vec2u { |
| fn add_assign(&mut self, rhs: Self) { |
| self.x = self.x.wrapping_add(rhs.x); |
| self.y = self.y.wrapping_add(rhs.y); |
| } |
| } |
| |
| impl Add for Vec2u { |
| type Output = Self; |
| fn add(self, rhs: Self) -> Self { |
| vec2u(self.x + rhs.x, self.y + rhs.y) |
| } |
| } |
| |
| impl Sub for Vec2u { |
| type Output = Self; |
| fn sub(self, rhs: Self) -> Self { |
| vec2u(self.x - rhs.x, self.y - rhs.y) |
| } |
| } |
| |
| impl SubAssign for Vec2u { |
| fn sub_assign(&mut self, rhs: Self) { |
| self.x -= rhs.x; |
| self.y -= rhs.y; |
| } |
| } |
| |
| fn get_msaa_mask(p0: Vec2f, p1: Vec2f, lut: &[u32]) -> u32 { |
| let dir = p1 - p0; |
| let n_unnorm = (dir.y, -dir.x); |
| let len = n_unnorm.0.hypot(n_unnorm.1).max(1e-9); |
| let n = (n_unnorm.0 / len, n_unnorm.1 / len); |
| |
| let c_raw = (n.0 * p0.x + n.1 * p0.y) - 0.5 * (n.0 + n.1); |
| let sign_c = if c_raw < 0.0 { -1.0 } else { 1.0 }; |
| let c2 = (1.0 - c_raw * PACKING_SCALE * sign_c).max(0.0); |
| let n_rev_scale = c2 * sign_c; |
| let n_rev = (n.0 * n_rev_scale, n.1 * n_rev_scale); |
| |
| let mut uv = (n_rev.0 * 0.5 + 0.5, n_rev.1 * 0.5 + 0.5); |
| if sign_c < 0.0 && uv.0 == 0.5 { |
| uv.0 = 0.5 - 1e-7; |
| } |
| |
| let mask_w = MASK_WIDTH as i32; |
| let mask_h = MASK_HEIGHT as i32; |
| let u_raw = (uv.0 * mask_w as f32).floor() as i32; |
| let v_raw = (uv.1 * mask_h as f32).floor() as i32; |
| let u = u_raw.max(0).min(mask_w - 1) as u32; |
| let v = v_raw.max(0).min(mask_h - 1) as u32; |
| |
| let index = v * MASK_WIDTH + u; |
| let shift = (index & 3) * 8; |
| (lut[(index >> 2) as usize] >> shift) & 0xff |
| } |
| |
| fn gpu_non_zero( |
| pmt_buf: &mut Vec<PreMergeTile>, |
| alpha_buf: &mut Vec<u8>, |
| lut: &[u32], |
| start_index: usize, |
| ) { |
| if pmt_buf.is_empty() || start_index >= pmt_buf.len() { |
| return; |
| } |
| |
| let aligned_start = start_index & !(PART_SIZE as usize - 1); |
| |
| let pmt_count = pmt_buf.len() - aligned_start; |
| let wg_count = ((pmt_count as u32) + PART_SIZE - 1) / PART_SIZE; |
| let total_threads = (wg_count * BLOCK_DIM) as usize; |
| |
| let mut temp_mask = vec![ |
| [[[0x80808080u32; PACK as usize]; Tile::HEIGHT as usize]; |
| Tile::WIDTH as usize]; |
| total_threads |
| ]; |
| |
| for wgid in 0..wg_count { |
| for tid in 0..BLOCK_DIM { |
| let gid = tid + wgid * BLOCK_DIM; |
| let pmt_index = (gid >> 2) as usize; |
| if pmt_index + aligned_start < start_index || pmt_index >= pmt_count { |
| continue; |
| } |
| |
| let row: u32 = gid & 3; |
| let row_index = row as usize; |
| let pmt = if pmt_index < pmt_count { |
| pmt_buf[pmt_index + aligned_start] |
| } else { |
| PreMergeTile { |
| alpha_index: 0, |
| packed_winding_line_idx: 0, |
| packed_scanned_winding: 0, |
| packed_xy: 0, |
| line: Line { |
| p0: Point { x: 0.0, y: 0.0 }, |
| p1: Point { x: 0.0, y: 0.0 }, |
| }, |
| } |
| }; |
| |
| let line = pmt.line; |
| let p0_x = line.p0.x; |
| let p0_y = line.p0.y; |
| let p1_x = line.p1.x; |
| let p1_y = line.p1.y; |
| |
| let dx = p1_x - p0_x; |
| let dy = p1_y - p0_y; |
| let is_vertical = dx.abs() <= f32::EPSILON; |
| let is_horizontal = dy.abs() <= f32::EPSILON; |
| let idx = select(1.0 / dx, 0.0, is_vertical); |
| let idy = select(1.0 / dy, 0.0, is_horizontal); |
| let dxdy = dx * idy; |
| let dydx = dy * idx; |
| |
| let canonical_x_dir = p1_x >= p0_x; |
| let canonical_y_dir = p1_y >= p0_y; |
| |
| // If the rightmost point of a line is left of the viewport, no further processing is |
| // required. The coarse mask is the only dependency which need be passed on. |
| let right = if canonical_x_dir { p1_x } else { p0_x }; |
| let right_in_viewport = right >= 0.0; |
| |
| let mut clipped_top = vec2f(0f32, 0f32); |
| let mut clipped_bot = vec2f(0f32, 0f32); |
| let intersection_data = pmt.intersection_data(); |
| if right_in_viewport { |
| let p0 = vec2f(p0_x, p0_y); |
| let p1 = vec2f(p1_x, p1_y); |
| let mut p_entry = p0; |
| let mut p_exit = p1; |
| |
| let tile_min_x_u32 = (pmt.tile_x() * Tile::WIDTH) as u32; |
| let tile_min_y_u32 = (pmt.tile_y() * Tile::HEIGHT) as u32; |
| |
| let tile_min = vec2f(tile_min_x_u32 as f32, tile_min_y_u32 as f32); |
| let tile_max = vec2f( |
| (tile_min_x_u32 + (Tile::WIDTH as u32)) as f32, |
| (tile_min_y_u32 + (Tile::HEIGHT as u32)) as f32, |
| ); |
| |
| let x_canon = vec2bool(canonical_x_dir); |
| let v_masks = select( |
| vec2u(INTERSECTS_RIGHT_MASK, INTERSECTS_LEFT_MASK), |
| vec2u(INTERSECTS_LEFT_MASK, INTERSECTS_RIGHT_MASK), |
| x_canon, |
| ); |
| let v_bounds = select( |
| vec2f(tile_max.x, tile_min.x), |
| vec2f(tile_min.x, tile_max.x), |
| x_canon, |
| ); |
| let mask_v_in = v_masks.x; |
| let mask_v_out = v_masks.y; |
| let bound_v_in = v_bounds.x; |
| let bound_v_out = v_bounds.y; |
| |
| let y_canon = vec2bool(canonical_y_dir); |
| let h_masks = select( |
| vec2u(INTERSECTS_BOTTOM_MASK, INTERSECTS_TOP_MASK), |
| vec2u(INTERSECTS_TOP_MASK, INTERSECTS_BOTTOM_MASK), |
| y_canon, |
| ); |
| let h_bounds = select( |
| vec2f(tile_max.y, tile_min.y), |
| vec2f(tile_min.y, tile_max.y), |
| y_canon, |
| ); |
| let mask_h_in = h_masks.x; |
| let mask_h_out = h_masks.y; |
| let bound_h_in = h_bounds.x; |
| let bound_h_out = h_bounds.y; |
| |
| // Entry intersection |
| let entry_hits = intersection_data & (mask_v_in | mask_h_in); |
| { |
| let use_h = (intersection_data & mask_h_in) != 0; |
| let p_oriented = select(p0.yx(), p0.xy(), use_h); |
| let bound = select(bound_v_in, bound_h_in, use_h); |
| let slope = select(dydx, dxdy, use_h); |
| let calculated = p_oriented.x + (bound - p_oriented.y) * slope; |
| let candidate = |
| select(vec2f(bound, calculated), vec2f(calculated, bound), use_h); |
| p_entry = select(p_entry, candidate, entry_hits != 0); |
| } |
| |
| // Exit intersection |
| let exit_hits = intersection_data & (mask_v_out | mask_h_out); |
| { |
| let use_h = (intersection_data & mask_h_out) != 0; |
| let p_oriented = select(p0.yx(), p0.xy(), use_h); |
| let bound = select(bound_v_out, bound_h_out, use_h); |
| let slope = select(dydx, dxdy, use_h); |
| let calculated = p_oriented.x + (bound - p_oriented.y) * slope; |
| let candidate = |
| select(vec2f(bound, calculated), vec2f(calculated, bound), use_h); |
| p_exit = select(p_exit, candidate, exit_hits != 0); |
| } |
| |
| // This is redundant, see shader comment. |
| // Perfect corner logic |
| // if (intersection_data & PERFECT_MASK) != 0 && (exit_hits ^ entry_hits) != 0 { |
| // let entry_is_empty = entry_hits == 0; |
| |
| // let target_val = select(p_exit, p_entry, entry_is_empty); |
| // let out_of_bounds = |
| // any(target_val.lt(tile_min)) || any(target_val.gt(tile_max)); |
| |
| // p_entry = select(p_entry, p_exit, out_of_bounds && entry_is_empty); |
| // p_exit = select(p_exit, p_entry, out_of_bounds && !entry_is_empty); |
| // } |
| |
| { |
| let exit_is_min = p_exit.y < p_entry.y; |
| clipped_top = select(p_entry, p_exit, exit_is_min); |
| clipped_bot = select(p_exit, p_entry, exit_is_min); |
| |
| clipped_top -= tile_min; |
| clipped_bot -= tile_min; |
| |
| let tile_size = vec2f(Tile::WIDTH as f32, Tile::HEIGHT as f32); |
| let zero_vec = vec2f_s(0.0); |
| |
| // Clamping has a dual purpose here: |
| // 1) Points which are slightly outside the tile due to floating point error are |
| // coerced inside. |
| // 2) More subtly, perfectly horizontal or vertical lines have their reciprocal |
| // derivatives set to 0. This causes the intersection calculation to return |
| // the original coordinate. While the coordinate fixed to the tile edge is |
| // explicitly set (and guaranteed valid), clamping forces the coordinate |
| // along that edge to be in bounds and watertight. |
| clipped_top = clamp(clipped_top, zero_vec, tile_size); |
| clipped_bot = clamp(clipped_bot, zero_vec, tile_size); |
| } |
| } |
| |
| let mut mask = [[0x80808080u32; PACK as usize]; Tile::WIDTH as usize]; |
| if pmt.tile_start_id() as u32 == (tid >> 2) { |
| let winding = pmt.scanned_winding(); |
| for x in 0..Tile::WIDTH as usize { |
| mask[x][0] += winding * 0x1010101u32; |
| mask[x][1] += winding * 0x1010101u32; |
| } |
| } |
| |
| if !right_in_viewport { |
| for x in 0..Tile::WIDTH as usize { |
| temp_mask[pmt_index][x][row_index][0] = mask[x][0]; |
| temp_mask[pmt_index][x][row_index][1] = mask[x][1]; |
| } |
| continue; |
| } |
| |
| let left = (intersection_data & INTERSECTS_LEFT_MASK) != 0; |
| if left { |
| let y_edge = if clipped_top.x <= clipped_bot.x { |
| clipped_top.y |
| } else { |
| clipped_bot.y |
| }; |
| let v = if canonical_x_dir { |
| 0xfefefeffu32 // = 0u32.wrapping_sub(0x1010101u32) |
| } else { |
| 0x1010101u32 |
| }; |
| |
| let y_cross = y_edge.ceil() as u32; |
| if row >= y_cross { |
| for x in 0..Tile::WIDTH as usize { |
| mask[x][0] += v; |
| mask[x][1] += v; |
| } |
| } |
| } |
| |
| // Discard perfectly axis aligned horizontal lines as the vertical mask produces the correct |
| // value. This also ensures that the ceil call for end_y will always produce a value |
| // distinct from start_y. |
| if is_horizontal && clipped_top.y == clipped_top.y.floor() { |
| for x in 0..Tile::WIDTH as usize { |
| temp_mask[pmt_index][x][row_index][0] = mask[x][0]; |
| temp_mask[pmt_index][x][row_index][1] = mask[x][1]; |
| } |
| continue; |
| } |
| |
| let start_y = clipped_top.y.floor() as u32; |
| let end_y = clipped_bot.y.ceil() as u32; |
| let x_dir = clipped_top.x <= clipped_bot.x; |
| if row >= start_y && row < end_y { |
| let row_top = row as f32; |
| let row_bot = (row + 1u32) as f32; |
| let x_cross_top = clipped_top.x + (row_top - clipped_top.y) * dxdy; |
| let p_top = select( |
| vec2f(x_cross_top, row_top), |
| clipped_top, |
| clipped_top.y >= row_top, |
| ); |
| |
| let x_cross_bot = clipped_top.x + (row_bot - clipped_top.y) * dxdy; |
| let p_bot = select( |
| vec2f(x_cross_bot, row_bot), |
| clipped_bot, |
| clipped_bot.y <= row_bot, |
| ); |
| |
| if p_top.y <= p_bot.y && p_top.y < row_bot { |
| let y = row; |
| let x_min = p_top.x.min(p_bot.x); |
| let x_max = p_top.x.max(p_bot.x); |
| let x_start = (x_min.floor() as i32).clamp(0, Tile::WIDTH as i32 - 1) as usize; |
| let x_end = (x_max.floor() as i32).clamp(0, Tile::WIDTH as i32 - 1) as usize; |
| |
| for x in x_start..Tile::WIDTH as usize { |
| let crossed_top = y > start_y || p_top.y == p_top.y.floor(); |
| |
| if x <= x_end { |
| let px = x as f32; |
| let py = y as f32; |
| |
| let mut msaa_mask = get_msaa_mask( |
| vec2f(p_top.x - px, p_top.y - py), |
| vec2f(p_bot.x - px, p_bot.y - py), |
| &lut, |
| ); |
| |
| let is_start = x == x_start; |
| let is_end = x == x_end; |
| let cannonical_start = (x_dir && is_start) || (!x_dir && is_end); |
| let cannonical_end = (x_dir && is_end) || (!x_dir && is_start); |
| let line_top = cannonical_start && y == start_y; |
| |
| let pixel_top_touch = p_top.y != p_top.y.floor(); |
| |
| let bumped = (line_top && p_top.x == 0.0 && pixel_top_touch) |
| || (!line_top && pixel_top_touch && x_dir); |
| |
| if line_top && !bumped { |
| let mask_shift = (8.0 * (p_top.y - py)).round() as u32; |
| msaa_mask &= 0xffu32 << mask_shift; |
| } |
| |
| if cannonical_end && y == end_y - 1 && p_bot.x != 0.0 { |
| let mask_shift = (8.0 * (p_bot.y - py)).round() as u32; |
| msaa_mask &= !(0xffu32 << mask_shift); |
| } |
| |
| let mask_a = msaa_mask ^ (msaa_mask << 7u32); |
| let mask_b = mask_a ^ (mask_a << 14u32); |
| let mut mask_lo = mask_b & 0x1010101u32; |
| let mut mask_hi = mask_b >> 4 & 0x1010101u32; |
| |
| if bumped { |
| let ones = 0x1010101u32; |
| mask_lo = mask_lo.wrapping_sub(ones); |
| mask_hi = mask_hi.wrapping_sub(ones); |
| } |
| |
| if canonical_y_dir { |
| mask[x][0] += mask_lo; |
| mask[x][1] += mask_hi; |
| } else { |
| mask[x][0] -= mask_lo; |
| mask[x][1] -= mask_hi; |
| }; |
| } else { |
| if crossed_top { |
| if canonical_y_dir { |
| mask[x][0] += 0x1010101u32; |
| mask[x][1] += 0x1010101u32; |
| } else { |
| mask[x][0] += 0xfefefeffu32; |
| mask[x][1] += 0xfefefeffu32; |
| } |
| } |
| } |
| } |
| } |
| } |
| for x in 0..Tile::WIDTH as usize { |
| temp_mask[pmt_index][x][row_index][0] = mask[x][0]; |
| temp_mask[pmt_index][x][row_index][1] = mask[x][1]; |
| } |
| } |
| } |
| |
| let mut part_indicator = vec![false; wg_count as usize]; |
| let mut part_acc = vec![ |
| [[[0x80808080u32; PACK as usize]; Tile::HEIGHT as usize]; |
| Tile::WIDTH as usize]; |
| wg_count as usize |
| ]; |
| |
| // wasteful? yes |
| let mut final_mask = vec![ |
| [[[0x80808080u32; PACK as usize]; Tile::HEIGHT as usize]; |
| Tile::WIDTH as usize]; |
| total_threads |
| ]; |
| |
| // TODO if guards are added to prevent carry smearing across packed values, a separate trick can |
| // be used where the value is only dibiased once at the end. However, since we assume that |
| // winding values can never exceed [-128, 127], simply debiasing with a wrapping sub is fastest. |
| for wgid in 0..wg_count { |
| for tid in 0..PART_SIZE { |
| let gidu = (tid + wgid * PART_SIZE) as usize; |
| if gidu >= pmt_count || gidu + aligned_start < start_index { |
| continue; |
| } |
| |
| if tid != 0 { |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| let prev_lo = temp_mask[gidu - 1][x][y][0]; |
| let delta_lo = prev_lo.wrapping_sub(0x80808080u32); |
| temp_mask[gidu][x][y][0] = temp_mask[gidu][x][y][0].wrapping_add(delta_lo); |
| |
| let prev_hi = temp_mask[gidu - 1][x][y][1]; |
| let delta_hi = prev_hi.wrapping_sub(0x80808080u32); |
| temp_mask[gidu][x][y][1] = temp_mask[gidu][x][y][1].wrapping_add(delta_hi); |
| } |
| } |
| } |
| |
| let pmt = pmt_buf[gidu + aligned_start]; |
| |
| // Tiles which are not end tiles have no need for further processing |
| if !(pmt.is_end_tile() || tid == PART_SIZE - 1) { |
| continue; |
| } |
| |
| let tile_start_id = pmt.tile_start_id() as u32; |
| // If the tile start is inside our partition, we must subtract the reduction at the |
| // tile before it. |
| // A tile start at 0 has no predecessors, so there is no need to subtract predecessors. |
| if tile_start_id != INVALID_ID && tile_start_id != 0 { |
| let tile_id = (tile_start_id + wgid * PART_SIZE - 1) as usize; |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| let prev_lo = temp_mask[tile_id][x][y][0]; |
| let delta_lo = prev_lo.wrapping_sub(0x80808080u32); |
| final_mask[gidu][x][y][0] = temp_mask[gidu][x][y][0].wrapping_sub(delta_lo); |
| |
| let prev_hi = temp_mask[tile_id][x][y][1]; |
| let delta_hi = prev_hi.wrapping_sub(0x80808080u32); |
| final_mask[gidu][x][y][1] = temp_mask[gidu][x][y][1].wrapping_sub(delta_hi); |
| } |
| } |
| } else { |
| final_mask[gidu] = temp_mask[gidu]; |
| } |
| |
| // The final thread in a workgroup writes out state necessary for successor workgroups |
| if tid == PART_SIZE - 1 { |
| // If there is a tile start in this workgroup, the last thread must see a valid id. |
| part_indicator[wgid as usize] = pmt.tile_start_id() as u32 != INVALID_ID; |
| part_acc[wgid as usize] = final_mask[gidu]; |
| } |
| |
| // Now, the "stitching process," if the start tile is not within the workgroup, we need |
| // to traverse backwards until we reach it. |
| if tile_start_id == INVALID_ID && pmt.is_end_tile() { |
| let mut lookback_id = wgid - 1; |
| loop { |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| let prev_lo = part_acc[lookback_id as usize][x][y][0]; |
| let delta_lo = prev_lo.wrapping_sub(0x80808080u32); |
| final_mask[gidu][x][y][0] = |
| final_mask[gidu][x][y][0].wrapping_add(delta_lo); |
| |
| let prev_hi = part_acc[lookback_id as usize][x][y][1]; |
| let delta_hi = prev_hi.wrapping_sub(0x80808080u32); |
| final_mask[gidu][x][y][1] = |
| final_mask[gidu][x][y][1].wrapping_add(delta_hi); |
| } |
| } |
| |
| if part_indicator[lookback_id as usize] || lookback_id == 0 { |
| break; |
| } else { |
| lookback_id -= 1; |
| } |
| } |
| } |
| } |
| } |
| |
| for gid in 0..pmt_count { |
| if gid + aligned_start < start_index { |
| continue; |
| } |
| let pmt = pmt_buf[gid + aligned_start]; |
| if pmt.is_end_tile() { |
| let mut temp_fine = [[0f32; Tile::HEIGHT as usize]; Tile::WIDTH as usize]; |
| let bias_mask = 0x80808080u32; |
| let ones_mask = 0x01010101u32; |
| |
| if pmt.tile_x() > 0 { |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| let mut active_count = 0u32; |
| for &pack in &[final_mask[gid][x][y][0], final_mask[gid][x][y][1]] { |
| let diff = pack ^ bias_mask; |
| let subbed = diff.wrapping_sub(ones_mask); |
| let zero_byte_markers = subbed & (!diff) & bias_mask; |
| let active_byte_markers = zero_byte_markers ^ bias_mask; |
| let count_pack = |
| ((active_byte_markers >> 7).wrapping_mul(ones_mask)) >> 24; |
| active_count += count_pack; |
| } |
| |
| if active_count > 0 { |
| temp_fine[x][y] += (active_count as f32) * 31.875; |
| } |
| } |
| } |
| } |
| |
| let mut u8_vals = [0u8; 16]; |
| let mut i = 0; |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| u8_vals[i] = temp_fine[x][y].round() as u8; |
| i += 1; |
| } |
| } |
| |
| let dst_idx = pmt.alpha_index as usize; |
| if dst_idx + 16 <= alpha_buf.len() { |
| alpha_buf[dst_idx..dst_idx + 16].copy_from_slice(&u8_vals); |
| } |
| } |
| } |
| } |
| |
| //CPU Style |
| |
| /// Clips a line segment to a tile. |
| /// |
| /// Returns the start and end points of the clipped line segment relative to the tile origin. |
| pub fn clip_to_tile( |
| line: &Line, |
| bounds: &[f32; 4], |
| derivatives: &[f32; 4], |
| intersection_data: u32, |
| cannonical_x_dir: bool, |
| cannonical_y_dir: bool, |
| ) -> [[f32; 2]; 2] { |
| const INTERSECTS_TOP_MASK: u32 = 1; |
| const INTERSECTS_BOTTOM_MASK: u32 = 2; |
| const INTERSECTS_LEFT_MASK: u32 = 4; |
| const INTERSECTS_RIGHT_MASK: u32 = 8; |
| const PERFECT_MASK: u32 = 16; |
| |
| let mut p_entry = [line.p0.x, line.p0.y]; |
| let mut p_exit = [line.p1.x, line.p1.y]; |
| |
| let tile_min_x = bounds[0]; |
| let tile_min_y = bounds[1]; |
| let tile_max_x = bounds[2]; |
| let tile_max_y = bounds[3]; |
| |
| let dx = derivatives[0]; |
| let dy = derivatives[1]; |
| |
| let (mask_v_in, bound_v_in, mask_v_out, bound_v_out) = if cannonical_x_dir { |
| ( |
| INTERSECTS_LEFT_MASK, |
| tile_min_x, |
| INTERSECTS_RIGHT_MASK, |
| tile_max_x, |
| ) |
| } else { |
| ( |
| INTERSECTS_RIGHT_MASK, |
| tile_max_x, |
| INTERSECTS_LEFT_MASK, |
| tile_min_x, |
| ) |
| }; |
| |
| let (mask_h_in, bound_h_in, mask_h_out, bound_h_out) = if cannonical_y_dir { |
| ( |
| INTERSECTS_TOP_MASK, |
| tile_min_y, |
| INTERSECTS_BOTTOM_MASK, |
| tile_max_y, |
| ) |
| } else { |
| ( |
| INTERSECTS_BOTTOM_MASK, |
| tile_max_y, |
| INTERSECTS_TOP_MASK, |
| tile_min_y, |
| ) |
| }; |
| |
| // let slope_v = dy * derivatives[2]; |
| // let slope_h = dx * derivatives[3]; |
| // let entry_hits = intersection_data & (mask_v_in | mask_h_in); |
| // if entry_hits != 0 { |
| // let use_h = (intersection_data & mask_h_in) != 0; |
| |
| // let base = if use_h { line.p0.x } else { line.p0.y }; |
| // let ortho = if use_h { line.p0.y } else { line.p0.x }; |
| // let bound = if use_h { bound_h_in } else { bound_v_in }; |
| // let slope = if use_h { slope_h } else { slope_v }; |
| |
| // let calculated = base + (bound - ortho) * slope; |
| // let axis = if use_h { 0 } else { 1 }; |
| |
| // p_entry[axis] = calculated; |
| // p_entry[1 - axis] = bound; |
| // } |
| |
| // let exit_hits = intersection_data & (mask_v_out | mask_h_out); |
| // if exit_hits != 0 { |
| // let use_h = (intersection_data & mask_h_out) != 0; |
| |
| // let base = if use_h { line.p0.x } else { line.p0.y }; |
| // let ortho = if use_h { line.p0.y } else { line.p0.x }; |
| // let bound = if use_h { bound_h_out } else { bound_v_out }; |
| // let slope = if use_h { slope_h } else { slope_v }; |
| |
| // let calculated = base + (bound - ortho) * slope; |
| // let axis = if use_h { 0 } else { 1 }; |
| |
| // p_exit[axis] = calculated; |
| // p_exit[1 - axis] = bound; |
| // } |
| |
| let idx = derivatives[2]; |
| let idy = derivatives[3]; |
| |
| let entry_hits = intersection_data & (mask_v_in | mask_h_in); |
| if entry_hits != 0 { |
| let use_h = (intersection_data & mask_h_in) != 0; |
| |
| let bound = if use_h { bound_h_in } else { bound_v_in }; |
| let start = if use_h { line.p0.y } else { line.p0.x }; |
| let inv_d = if use_h { idy } else { idx }; |
| |
| let t = (bound - start) * inv_d; |
| |
| p_entry[0] = line.p0.x + t * dx; |
| p_entry[1] = line.p0.y + t * dy; |
| p_entry[if use_h { 1 } else { 0 }] = bound; |
| } |
| |
| let exit_hits = intersection_data & (mask_v_out | mask_h_out); |
| if exit_hits != 0 { |
| let use_h = (intersection_data & mask_h_out) != 0; |
| |
| let bound = if use_h { bound_h_out } else { bound_v_out }; |
| let start = if use_h { line.p0.y } else { line.p0.x }; |
| let inv_d = if use_h { idy } else { idx }; |
| |
| let t = (bound - start) * inv_d; |
| |
| p_exit[0] = line.p0.x + t * dx; |
| p_exit[1] = line.p0.y + t * dy; |
| p_exit[if use_h { 1 } else { 0 }] = bound; |
| } |
| |
| // TODO: The perfect bit can be removed; this means less cpu side work, but this check becomes |
| // less specific, so EVERY single intersection hits the bounds checks |
| // |
| // If we have a perfect corner intersection (PERFECT_MASK is set) AND the intersection has been |
| // tie-broken to a single edge (only 1 bit set in 0-3), we duplicate the intersection point. |
| // Otherwise, the raw endpoint would be returned. In these single intersection cases, this |
| // creates valid non-deleterious logic. |
| let is_perfect = (intersection_data & PERFECT_MASK) != 0; |
| let single_hit = (exit_hits ^ entry_hits) != 0; |
| if is_perfect && single_hit { |
| let (target, source) = if entry_hits == 0 { |
| (&mut p_entry, p_exit) |
| } else { |
| (&mut p_exit, p_entry) |
| }; |
| if target[0] < tile_min_x |
| || target[0] > tile_max_x |
| || target[1] < tile_min_y |
| || target[1] > tile_max_y |
| { |
| *target = source; |
| } |
| } |
| |
| let mut result = if p_exit[1] >= p_entry[1] { |
| [p_entry, p_exit] |
| } else { |
| [p_exit, p_entry] |
| }; |
| |
| // if result[0][1] > result[1][1] { |
| // panic!("Not ordered correctly! Intersected: {} {} Original Line ({},{}) ({},{})", |
| // result[0][1], result[1][1], p0[0], p0[1], p1[0], p1[1]); |
| // } |
| |
| result[0][0] -= tile_min_x; |
| result[0][1] -= tile_min_y; |
| result[1][0] -= tile_min_x; |
| result[1][1] -= tile_min_y; |
| |
| // Clamping has a dual purpose here: |
| // 1) Points which are slightly outside the tile due to floating point error are coerced inside. |
| // 2) More subtly, perfectly horizontal or vertical lines have their reciprocal derivatives |
| // set to 0. This causes the intersection calculation to return the original coordinate. |
| // While the coordinate fixed to the tile edge is explicitly set (and guaranteed valid), |
| // clamping forces the coordinate along that edge to be in bounds and watertight. |
| let width = Tile::WIDTH as f32; |
| let height = Tile::HEIGHT as f32; |
| result[0][0] = result[0][0].clamp(0.0, width); |
| result[0][1] = result[0][1].clamp(0.0, height); |
| result[1][0] = result[1][0].clamp(0.0, width); |
| result[1][1] = result[1][1].clamp(0.0, height); |
| |
| result |
| } |
| |
| fn msaa_merge_even_odd(pmt_buf: &mut Vec<PreMergeTile>, alpha_buf: &mut Vec<u8>, lut: &Vec<u8>) { |
| if pmt_buf.is_empty() { |
| return; |
| } |
| |
| let pmt_count = pmt_buf.len(); |
| let mut t_vert_mask = vec![[0u8; Tile::HEIGHT as usize]; pmt_count]; |
| let mut temp_mask = vec![[[0u8; Tile::HEIGHT as usize]; Tile::WIDTH as usize]; pmt_count]; |
| |
| for gid in 0..pmt_count { |
| let pmt = pmt_buf[gid]; |
| |
| let line = pmt.line; |
| let p0_x = line.p0.x; |
| let p0_y = line.p0.y; |
| let p1_x = line.p1.x; |
| let p1_y = line.p1.y; |
| |
| let tile_min_x_u32 = (pmt.tile_x() as u32) * (Tile::WIDTH as u32); |
| let tile_min_y_u32 = (pmt.tile_y() as u32) * (Tile::HEIGHT as u32); |
| |
| let tile_min_x_px = tile_min_x_u32 as f32; |
| let tile_min_y_px = tile_min_y_u32 as f32; |
| let tile_max_x_px = (tile_min_x_u32 + (Tile::WIDTH as u32)) as f32; |
| let tile_max_y_px = (tile_min_y_u32 + (Tile::HEIGHT as u32)) as f32; |
| |
| let bounds = [tile_min_x_px, tile_min_y_px, tile_max_x_px, tile_max_y_px]; |
| let intersection_data = pmt.intersection_data(); |
| |
| let cannonical_x_dir = p1_x >= p0_x; |
| let cannonical_y_dir = p1_y >= p0_y; |
| |
| let dx = p1_x - p0_x; |
| let dy = p1_y - p0_y; |
| |
| let is_vertical = dx.abs() <= f32::EPSILON; |
| let is_horizontal = dy.abs() <= f32::EPSILON; |
| |
| let idx = if !is_vertical { 1.0 / dx } else { 0.0 }; |
| let idy = if !is_horizontal { 1.0 / dy } else { 0.0 }; |
| |
| let derivatives = [dx, dy, idx, idy]; |
| let clipped_result = clip_to_tile( |
| &line, |
| &bounds, |
| &derivatives, |
| intersection_data, |
| cannonical_x_dir, |
| cannonical_y_dir, |
| ); |
| |
| let [local_p0, local_p1] = clipped_result; |
| |
| let left = (intersection_data & INTERSECTS_LEFT_MASK) != 0; |
| |
| // A edge is considered as intersecting the left edge if: |
| // 1) It passes through the left tile, i.e. a point that only touches the left edge does |
| // not count. |
| // 2) It does not also intersect the top edge at the corner. i.e. perfect intersection |
| // 3) It is not a vertical line. This is a property of the tiling algorithm, so no check is |
| // needed here. |
| if left { |
| let y_edge = if local_p0[0] <= local_p1[0] { |
| local_p0[1] |
| } else { |
| local_p1[1] |
| }; |
| let y_cross = y_edge.ceil() as u16; |
| for y in y_cross as usize..Tile::HEIGHT as usize { |
| t_vert_mask[gid][y] = 0xffu8; |
| } |
| } |
| |
| // Start tiles get the coarse winding number |
| if pmt.fill_rule() { |
| let fill = (pmt.scanned_winding() % 2) != 0; |
| let coarse_mask = if fill { 0xffu8 } else { 0u8 }; |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| temp_mask[gid][x][y] = coarse_mask; |
| } |
| } |
| } |
| |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| temp_mask[gid][x][y] ^= t_vert_mask[gid][y]; |
| } |
| } |
| |
| // Discard perfectly axis aligned horizontal lines as the vertical mask produces the correct |
| // value. This also ensures that the ceil call for end_y will always produce a value |
| // distinct from start_y. |
| if is_horizontal && local_p0[1] == local_p0[1].floor() { |
| continue; |
| } |
| |
| let start_y = local_p0[1].floor() as usize; |
| let end_y = local_p1[1].ceil() as usize; |
| let mut top_row = [[f32::NAN; 2]; (Tile::HEIGHT + 1) as usize]; |
| { |
| // if end_y > Tile::HEIGHT as usize { |
| // println!("BAD TILE at ({}, {})", pmt.tile_x(), pmt.tile_y()); |
| // println!("Line: ({}, {}) to ({}, {})", p0_x, p0_y, p1_x, p1_y); |
| // println!("Intersection Data bits: {:05b}", pmt.intersection_data()); |
| // println!("Calculated local_p1: {:?}", local_p1); |
| // println!("startY EndY {}, {}", start_y, end_y); |
| // } |
| debug_assert!(end_y <= (Tile::HEIGHT as usize) && start_y <= end_y); |
| let dydx = if is_horizontal { 0.0 } else { dx / dy }; |
| top_row[start_y] = local_p0; |
| for y_idx in (start_y + 1)..end_y { |
| let grid_y = y_idx as f32; |
| let grid_x = local_p0[0] + (grid_y - local_p0[1]) * dydx; |
| top_row[y_idx] = [grid_x, grid_y]; |
| } |
| top_row[end_y] = local_p1; |
| } |
| |
| let x_dir = local_p0[0] <= local_p1[0]; |
| for y in start_y..end_y { |
| let p_top = top_row[y]; |
| let p_bottom = top_row[y + 1]; |
| |
| if p_top[0].is_nan() || p_bottom[0].is_nan() { |
| continue; |
| } |
| |
| let x_min = p_top[0].min(p_bottom[0]); |
| let x_max = p_top[0].max(p_bottom[0]); |
| let x_start = (x_min.floor() as i32).clamp(0, Tile::WIDTH as i32 - 1) as usize; |
| let x_end = |
| (x_max.next_down().floor() as i32).clamp(0, Tile::WIDTH as i32 - 1) as usize; |
| let crossed_top = y > start_y || p_top[1] == p_top[1].floor(); |
| for x in x_start..Tile::WIDTH as usize { |
| if x <= x_end { |
| let px = x as f32; |
| let py = y as f32; |
| |
| let mut mask = algorithm_lut_mask_halfplane( |
| [(p_top[0] - px), (p_top[1] - py)], |
| [(p_bottom[0] - px), (p_bottom[1] - py)], |
| &lut, |
| ); |
| |
| let is_start = x == x_start; |
| let is_end = x == x_end; |
| let cannonical_start = (x_dir && is_start) || (!x_dir && is_end); |
| let cannonical_end = (x_dir && is_end) || (!x_dir && is_start); |
| let line_top = cannonical_start && y == start_y; |
| let bumped = (line_top && p_top[0] == 0.0 && left) |
| || (!line_top && p_top[1] != p_top[1].floor() && x_dir); |
| if line_top && !bumped { |
| let mask_shift = (8.0 * (p_top[1] - py)).round() as u32; |
| mask &= (0xffu32 << mask_shift) as u8; |
| } |
| |
| if cannonical_end && y == end_y - 1 && p_bottom[0] != 0.0 { |
| let mask_shift = (8.0 * (p_bottom[1] - py)).round() as u32; |
| mask &= !((0xffu32 << mask_shift) as u8); |
| } |
| |
| if bumped { |
| mask ^= 0xffu8; |
| } |
| |
| temp_mask[gid][x][y] ^= mask; |
| } else { |
| if crossed_top { |
| temp_mask[gid][x][y] ^= 0xffu8; |
| } |
| } |
| } |
| } |
| } |
| |
| for gid in 1..pmt_count { |
| let pmt = pmt_buf[gid]; |
| if !pmt.fill_rule() { |
| // If i am NOT a start tile . . . layer |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| temp_mask[gid][x][y] ^= temp_mask[gid - 1][x][y]; |
| } |
| } |
| } |
| } |
| |
| for gid in 0..(pmt_count - 1) { |
| let pmt = pmt_buf[gid]; |
| if pmt.is_end_tile() { |
| let mut temp_fine = [[0f32; Tile::HEIGHT as usize]; Tile::WIDTH as usize]; |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| temp_fine[x][y] = |
| (temp_mask[gid][x][y].count_ones() as f32 * 31.875).min(255.0); |
| } |
| } |
| |
| let mut u8_vals = [0u8; 16]; |
| let mut i = 0; |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| u8_vals[i] = temp_fine[x][y].round() as u8; |
| i += 1; |
| } |
| } |
| |
| // this will be indexed into |
| alpha_buf.extend_from_slice(&u8_vals); |
| } |
| } |
| } |
| |
| const PACK: u32 = 2; |
| fn msaa_merge_non_zero(pmt_buf: &mut Vec<PreMergeTile>, alpha_buf: &mut Vec<u8>, lut: &Vec<u8>) { |
| if pmt_buf.is_empty() { |
| return; |
| } |
| |
| let pmt_count = pmt_buf.len(); |
| let mut t_vert_mask = vec![[0u32; Tile::HEIGHT as usize]; pmt_count]; |
| let mut temp_mask = vec![ |
| [[[0x80808080u32; PACK as usize]; Tile::HEIGHT as usize]; |
| Tile::WIDTH as usize]; |
| pmt_count |
| ]; |
| |
| for gid in 0..pmt_count { |
| let pmt = pmt_buf[gid]; |
| |
| let line = pmt.line; |
| let p0_x = line.p0.x; |
| let p0_y = line.p0.y; |
| let p1_x = line.p1.x; |
| let p1_y = line.p1.y; |
| |
| let cannonical_x_dir = p1_x >= p0_x; |
| let cannonical_y_dir = p1_y >= p0_y; |
| |
| // Start tiles get the coarse winding number |
| if pmt.fill_rule() { |
| let winding = pmt.scanned_winding(); |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| temp_mask[gid][x][y][0] += winding * 0x1010101u32; |
| temp_mask[gid][x][y][1] += winding * 0x1010101u32; |
| } |
| } |
| } |
| |
| // If the rightmost point of a line is left of the viewport, no further processing is |
| // required. The coarse mask is the only dependency which need be passed on. |
| let right = if cannonical_x_dir { p1_x } else { p0_x }; |
| |
| if right < 0.0 { |
| continue; |
| } |
| |
| let tile_min_x_u32 = (pmt.tile_x() as u32) * (Tile::WIDTH as u32); |
| let tile_min_y_u32 = (pmt.tile_y() as u32) * (Tile::HEIGHT as u32); |
| |
| let tile_min_x_px = tile_min_x_u32 as f32; |
| let tile_min_y_px = tile_min_y_u32 as f32; |
| let tile_max_x_px = (tile_min_x_u32 + (Tile::WIDTH as u32)) as f32; |
| let tile_max_y_px = (tile_min_y_u32 + (Tile::HEIGHT as u32)) as f32; |
| |
| let bounds = [tile_min_x_px, tile_min_y_px, tile_max_x_px, tile_max_y_px]; |
| let intersection_data = pmt.intersection_data(); |
| |
| let dx = p1_x - p0_x; |
| let dy = p1_y - p0_y; |
| |
| let is_vertical = dx.abs() <= f32::EPSILON; |
| let is_horizontal = dy.abs() <= f32::EPSILON; |
| |
| let idx = if !is_vertical { 1.0 / dx } else { 0.0 }; |
| let idy = if !is_horizontal { 1.0 / dy } else { 0.0 }; |
| |
| let derivatives = [dx, dy, idx, idy]; |
| let clipped_result = clip_to_tile( |
| &line, |
| &bounds, |
| &derivatives, |
| intersection_data, |
| cannonical_x_dir, |
| cannonical_y_dir, |
| ); |
| |
| let [local_p0, local_p1] = clipped_result; |
| //println!("({}, {}) ({}, {})", local_p0[0], local_p0[1], local_p1[0], local_p1[1]); |
| |
| let left = (intersection_data & INTERSECTS_LEFT_MASK) != 0; |
| if left { |
| let y_edge = if local_p0[0] <= local_p1[0] { |
| local_p0[1] |
| } else { |
| local_p1[1] |
| }; |
| let v = if cannonical_x_dir { |
| 0xfefefeffu32 // = 0u32.wrapping_sub(0x1010101u32) |
| } else { |
| 0x1010101u32 |
| }; |
| |
| let y_cross = y_edge.ceil() as u16; |
| for y in y_cross as usize..Tile::HEIGHT as usize { |
| t_vert_mask[gid][y] = v; |
| } |
| } |
| |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| temp_mask[gid][x][y][0] += t_vert_mask[gid][y]; |
| temp_mask[gid][x][y][1] += t_vert_mask[gid][y]; |
| } |
| } |
| |
| // Discard perfectly axis aligned horizontal lines as the vertical mask produces the correct |
| // value. This also ensures that the ceil call for end_y will always produce a value |
| // distinct from start_y. |
| if is_horizontal && local_p0[1] == local_p0[1].floor() { |
| continue; |
| } |
| |
| let start_y = local_p0[1].floor() as usize; |
| let end_y = local_p1[1].ceil() as usize; |
| let mut top_row = [[f32::NAN; 2]; (Tile::HEIGHT + 1) as usize]; |
| { |
| // if end_y > Tile::HEIGHT as usize { |
| // println!("BAD TILE at ({}, {})", pmt.tile_x(), pmt.tile_y()); |
| // println!("Line: ({}, {}) to ({}, {})", p0_x, p0_y, p1_x, p1_y); |
| // println!("Intersection Data bits: {:05b}", pmt.intersection_data()); |
| // println!("Calculated local_p1: {:?}", local_p1); |
| // println!("startY EndY {}, {}", start_y, end_y); |
| // } |
| debug_assert!(end_y <= (Tile::HEIGHT as usize) && start_y <= end_y); |
| let dydx = if is_horizontal { 0.0 } else { dx / dy }; |
| top_row[start_y] = local_p0; |
| for y_idx in (start_y + 1)..end_y { |
| let grid_y = y_idx as f32; |
| let grid_x = local_p0[0] + (grid_y - local_p0[1]) * dydx; |
| top_row[y_idx] = [grid_x, grid_y]; |
| } |
| top_row[end_y] = local_p1; |
| } |
| |
| let x_dir = local_p0[0] <= local_p1[0]; |
| for y in start_y..end_y { |
| let p_top = top_row[y]; |
| let p_bottom = top_row[y + 1]; |
| |
| if p_top[0].is_nan() || p_bottom[0].is_nan() { |
| continue; |
| } |
| |
| let x_min = p_top[0].min(p_bottom[0]); |
| let x_max = p_top[0].max(p_bottom[0]); |
| let x_start = (x_min.floor() as i32).clamp(0, Tile::WIDTH as i32 - 1) as usize; |
| let x_end = (x_max.floor() as i32).clamp(0, Tile::WIDTH as i32 - 1) as usize; |
| |
| //println!("({}, {}) ({}, {})", p_top[0], p_top[1], p_bottom[1], p_bottom[1]); |
| |
| for x in x_start..Tile::WIDTH as usize { |
| let crossed_top = y > start_y || p_top[1] == p_top[1].floor(); |
| |
| // Does this matter? |
| //let crossed_top = y > start_y || (y == 0 && pmt.intersects_top()) || p_top[1] == p_top[1].floor(); |
| |
| if x <= x_end { |
| let px = x as f32; |
| let py = y as f32; |
| |
| let mut mask = algorithm_lut_mask_halfplane( |
| [(p_top[0] - px), (p_top[1] - py)], |
| [(p_bottom[0] - px), (p_bottom[1] - py)], |
| &lut, |
| ) as u32; |
| |
| let is_start = x == x_start; |
| let is_end = x == x_end; |
| let cannonical_start = (x_dir && is_start) || (!x_dir && is_end); |
| let cannonical_end = (x_dir && is_end) || (!x_dir && is_start); |
| let line_top = cannonical_start && y == start_y; |
| |
| let pixel_top_touch = p_top[1] != p_top[1].floor(); |
| let bumped = (line_top && p_top[0] == 0.0 && pixel_top_touch) |
| || (!line_top && pixel_top_touch && x_dir); |
| if line_top && !bumped { |
| let mask_shift = (8.0 * (p_top[1] - py)).round() as u32; |
| mask &= 0xffu32 << mask_shift; |
| } |
| |
| if cannonical_end && y == end_y - 1 && p_bottom[0] != 0.0 { |
| let mask_shift = (8.0 * (p_bottom[1] - py)).round() as u32; |
| mask &= !(0xffu32 << mask_shift); |
| } |
| |
| let mask_a = mask ^ (mask << 7u32); |
| let mask_b = mask_a ^ (mask_a << 14u32); |
| let mut mask_lo = mask_b & 0x1010101u32; |
| let mut mask_hi = mask_b >> 4 & 0x1010101u32; |
| |
| if bumped { |
| let ones = 0x1010101u32; |
| mask_lo = mask_lo.wrapping_sub(ones); |
| mask_hi = mask_hi.wrapping_sub(ones); |
| } |
| |
| if cannonical_y_dir { |
| temp_mask[gid][x][y][0] += mask_lo; |
| temp_mask[gid][x][y][1] += mask_hi; |
| } else { |
| temp_mask[gid][x][y][0] -= mask_lo; |
| temp_mask[gid][x][y][1] -= mask_hi; |
| }; |
| } else { |
| if crossed_top { |
| if cannonical_y_dir { |
| temp_mask[gid][x][y][0] += 0x1010101u32; |
| temp_mask[gid][x][y][1] += 0x1010101u32; |
| } else { |
| temp_mask[gid][x][y][0] += 0xfefefeffu32; // = 0u32.wrapping_sub(0x1010101u32) |
| temp_mask[gid][x][y][1] += 0xfefefeffu32; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| for gid in 1..pmt_count { |
| let pmt = pmt_buf[gid]; |
| if !pmt.fill_rule() { |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| let prev_lo = temp_mask[gid - 1][x][y][0]; |
| let delta_lo = prev_lo.wrapping_sub(0x80808080u32); |
| temp_mask[gid][x][y][0] = temp_mask[gid][x][y][0].wrapping_add(delta_lo); |
| |
| let prev_hi = temp_mask[gid - 1][x][y][1]; |
| let delta_hi = prev_hi.wrapping_sub(0x80808080u32); |
| temp_mask[gid][x][y][1] = temp_mask[gid][x][y][1].wrapping_add(delta_hi); |
| } |
| } |
| } |
| } |
| |
| for gid in 0..(pmt_count - 1) { |
| let pmt = pmt_buf[gid]; |
| if pmt.is_end_tile() { |
| let mut temp_fine = [[0f32; Tile::HEIGHT as usize]; Tile::WIDTH as usize]; |
| let bias_mask = 0x80808080u32; |
| let ones_mask = 0x01010101u32; |
| |
| if pmt.tile_x() > 0 { |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| let mut active_count = 0u32; |
| for &pack in &[temp_mask[gid][x][y][0], temp_mask[gid][x][y][1]] { |
| let diff = pack ^ bias_mask; |
| let subbed = diff.wrapping_sub(ones_mask); |
| let zero_byte_markers = subbed & (!diff) & bias_mask; |
| let active_byte_markers = zero_byte_markers ^ bias_mask; |
| let count_pack = |
| ((active_byte_markers >> 7).wrapping_mul(ones_mask)) >> 24; |
| active_count += count_pack; |
| } |
| if active_count > 0 { |
| temp_fine[x][y] += (active_count as f32) * 31.875; |
| } |
| } |
| } |
| } |
| |
| let mut u8_vals = [0u8; 16]; |
| let mut i = 0; |
| for x in 0..Tile::WIDTH as usize { |
| for y in 0..Tile::HEIGHT as usize { |
| u8_vals[i] = temp_fine[x][y].round() as u8; |
| i += 1; |
| } |
| } |
| |
| // this will be indexed into |
| alpha_buf.extend_from_slice(&u8_vals); |
| } |
| } |
| } |