| // Copyright 2025 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #![feature(portable_simd)] | |
| // Modules public for testing, don't expect stable API. | |
| mod cxx; | |
| pub mod decoder; | |
| pub mod dither; | |
| pub mod quant; | |
| pub mod selectors; | |
| use std::simd::prelude::*; | |
| use std::simd::Simd; | |
| use bytemuck::cast_slice; | |
| use crate::decoder::decode_etc1_block; | |
| use crate::dither::dither; | |
| use crate::quant::{quantize_averages, QuantResult}; | |
| use crate::selectors::search_table_and_selectors; | |
| // We primarily compute with 16-bit integers and a width of 8 fills a 128-bit | |
| // wide lane (SSE, NEON). TODO(b/393494744): When we introduce multiversioning | |
| // and support for AVX2 etc. this should be converted to a template parameter | |
| // that varies based on the target architecture. | |
| const SIMD_WIDTH: usize = 8; | |
| const HALF_WIDTH: usize = SIMD_WIDTH / 2; | |
| const QUARTER_WIDTH: usize = SIMD_WIDTH / 4; | |
| type Reg = Simd<i16, SIMD_WIDTH>; | |
| type Reg32 = Simd<i32, SIMD_WIDTH>; | |
| type UReg = Simd<u16, SIMD_WIDTH>; | |
| const ETC1_BLOCK_BYTES: usize = 8; | |
| /// Define a helper to interleave elements from two vectors, reinterpret | |
| /// it as a type twice as large, and return the resulting vector. | |
| /// Each argument / return value is an array of vectors; conceptually, this | |
| /// represents a vector that is <width> * <len> large; however, since std::simd | |
| /// types have upper limits on their width we represent them using arrays to be | |
| /// portable. | |
| macro_rules! define_interleave { | |
| ($fn_name:ident, $src_ty:ty, $dst_ty:ty, $src_width:expr, $dst_width:expr, $src_len:literal) => { | |
| fn $fn_name( | |
| a: [Simd<$src_ty, $src_width>; $src_len], | |
| b: [Simd<$src_ty, $src_width>; $src_len], | |
| ) -> [Simd<$dst_ty, $dst_width>; $src_len * 2] { | |
| let mut iter = (0..$src_len).flat_map(|i| { | |
| let (a, b) = a[i].interleave(b[i]); | |
| [a, b].map(|x| bytemuck::cast(x)) | |
| }); | |
| let res = std::array::from_fn(|_| iter.next().unwrap()); | |
| assert!(iter.next().is_none()); | |
| res | |
| } | |
| }; | |
| } | |
| /// Convert individual codewords laid out as [15..0, 31..16, 47..32, 63..48] | |
| /// into interleaved u64 arrays, while flipping the endianness (our internal | |
| /// representation is little endian while ETC1 requires big endian). | |
| #[inline] | |
| pub fn interleave_etc1(regs: [UReg; 4]) -> [Simd<u64, QUARTER_WIDTH>; 4] { | |
| // The interleaving assumes little endian. | |
| #[cfg(target_endian = "big")] | |
| compile_error!("Big endian is not supported"); | |
| define_interleave!(conv_16_to_32, u16, u32, SIMD_WIDTH, HALF_WIDTH, 1); | |
| define_interleave!(conv_32_to_64, u32, u64, HALF_WIDTH, QUARTER_WIDTH, 2); | |
| // Step 1: make each u16 codeword big-endian | |
| let regs = regs.map(|r| r.swap_bytes()); | |
| // Step 2: [aaaa, bbbb] to [baba, baba] | |
| let regs = [conv_16_to_32([regs[1]], [regs[0]]), conv_16_to_32([regs[3]], [regs[2]])]; | |
| // Step 3: [baba, baba], [dcdc, dcdc] to [dcba, dcba], [dcba, dcba] | |
| conv_32_to_64(regs[1], regs[0]) | |
| } | |
| /// Load `SIMD_WIDTH` blocks from a region `4*SIMD_WIDTH` wide and `4` tall, | |
| /// starting at `base_x` and `base_y`. | |
| /// | |
| /// Out of bounds pixels are padded with mirroring. For example, `abcdxy` | |
| /// becomes `abcdxyyx`. | |
| /// | |
| /// Returns a 3D array of SIMD vectors. Each block is mapped to a SIMD lane | |
| /// (from left to right), and each pixel in the block is accessed as | |
| /// `[y][x][channel]`. | |
| #[allow(clippy::needless_range_loop)] // Range loops seem more readable here. | |
| #[inline] | |
| pub fn load_input_block( | |
| src: &[u32], | |
| width: u32, | |
| height: u32, | |
| row_width: u32, | |
| base_x: u32, | |
| base_y: u32, | |
| ) -> [[[Reg; 3]; 4]; 4] { | |
| let mut data = [[[Reg::default(); 3]; 4]; 4]; | |
| // For now, input load and output store are not vectorized. The main reason | |
| // is that efficient loading requires shuffling and is poorly supported | |
| // by std::simd and the wide crate (which we plan to use for | |
| // supporting stable toolchain). Input load currently accounts for | |
| // ~20% of the runtime. If shuffle support improves this would be a | |
| // good candidate for optimization. | |
| for i in 0..4 { | |
| for j in 0..4 { | |
| let mut buf = [0u32; SIMD_WIDTH]; | |
| for block in 0..SIMD_WIDTH as u32 { | |
| let x = base_x + block * 4 + j as u32; | |
| let y = base_y + i as u32; | |
| buf[block as usize] = if x < width && y < height { | |
| // Fast path: load in-bound pixel | |
| src[(y * row_width + x) as usize] | |
| } else { | |
| // Slow path: mirror out-of-bound pixels | |
| // If width or height is 1, mirroring can overflow, so make | |
| // it saturate. | |
| let xm = if x >= width { (width - 1).saturating_sub(x - width) } else { x }; | |
| let ym = if y >= height { (height - 1).saturating_sub(y - height) } else { y }; | |
| src[(ym * row_width + xm) as usize] | |
| }; | |
| } | |
| let rgbx = Simd::from_array(buf); | |
| let extract_channel = |x: Simd<u32, SIMD_WIDTH>, shift: u32| { | |
| (x >> shift).cast::<i16>() & Simd::splat(0xFF) | |
| }; | |
| data[i][j][0] = extract_channel(rgbx, 0); | |
| data[i][j][1] = extract_channel(rgbx, 8); | |
| data[i][j][2] = extract_channel(rgbx, 16); | |
| } | |
| } | |
| data | |
| } | |
| /// Compress RGB pixels to ETC1. | |
| /// | |
| /// - `src` should be in RGBA format (the least significant byte is red). | |
| /// - `dst` will be filled with compressed ETC1 blocks. | |
| /// - `src_width` and `src_height` specifies the logical size of the image in | |
| /// pixels. These does not need to be multiple of 4. The boundary pixels will | |
| /// be padded with unspecified values. | |
| /// - `src_row_width` and `dst_row_width` specifies the in-memory length of each | |
| /// row, in pixels and blocks, respectively. | |
| /// | |
| /// Note that `src` takes an aligned 32-bit buffer while `dst` takes a byte | |
| /// buffer, even though each ETC1 codeword is 64-bit. This is due to two | |
| /// reasons: | |
| /// - 32-bit alignment is practical to get even on 32-bit platforms, whereas | |
| /// 64-bit values are not aligned to 8 bytes on 32-bit ARM. | |
| /// - We require extensive shuffling when loading inputs, but store to the | |
| /// output straight in the order of blocks. Dealing with unaligned buffers in | |
| /// the latter case is significantly easier. | |
| pub fn compress_etc1( | |
| src: &[u32], | |
| dst: &mut [u8], | |
| src_width: u32, | |
| src_height: u32, | |
| src_row_width: u32, | |
| dst_row_width: u32, | |
| ) { | |
| // Note: We deliberately do not declare the block size (4x4) of ETC1 as a | |
| // constant. While magic constants in general are discouraged, the | |
| // block size appears way too frequent that naming it would make the | |
| // code verbose and less readable. | |
| let dst_height = src_height.div_ceil(4); | |
| let dst_width = src_width.div_ceil(4); | |
| // Aligned staging buffer. Data is copied into the potentially unaligned | |
| // destination buffer at the end of the each row. | |
| let mut staging_row = vec![[Simd::splat(0); 4]; (dst_width as usize).div_ceil(SIMD_WIDTH)]; | |
| let copy_len = dst_width as usize * ETC1_BLOCK_BYTES; | |
| // Note on vectorization scheme: | |
| // | |
| // We process one 4x4 block per SIMD lane, instead of the more common | |
| // practice of processing pixels within the same block in parallel using | |
| // multiple lanes. The one-block-per-lane scheme, more akin to SPMD | |
| // programming, allows most of our code to be shuffle-free, and works | |
| // much better with portable SIMD than schemes that heavily shuffles. | |
| for dst_y in 0..dst_height { | |
| for dst_x0 in (0..dst_width).step_by(SIMD_WIDTH) { | |
| let data = | |
| load_input_block(src, src_width, src_height, src_row_width, dst_x0 * 4, dst_y * 4); | |
| let data = dither(&data); | |
| let QuantResult { lo: hdr0, hi: hdr1, scaled0: ep0, scaled1: ep1 } = | |
| quantize_averages(&data); | |
| let best_fit = search_table_and_selectors(hdr0, hdr1, &data, [ep0, ep1]); | |
| let codewords = interleave_etc1(best_fit); | |
| staging_row[dst_x0 as usize / SIMD_WIDTH] = codewords; | |
| } | |
| let dst_row = &mut dst[(dst_y * dst_row_width) as usize * ETC1_BLOCK_BYTES..]; | |
| let staging_row_bytes = cast_slice(&staging_row); | |
| dst_row[..copy_len].copy_from_slice(&staging_row_bytes[..copy_len]); | |
| } | |
| } | |
| /// Validates that `src` and `dst` slices are large enough for `decompress_etc1` | |
| /// using checked arithmetic to prevent 32-bit `usize` overflow on 32-bit | |
| /// platforms. | |
| fn validate_decompress_buffers( | |
| src_len: usize, | |
| dst_len: usize, | |
| dst_width: u32, | |
| dst_height: u32, | |
| src_row_width: u32, | |
| dst_row_width: u32, | |
| ) { | |
| let src_height = dst_height.div_ceil(4) as usize; | |
| let src_width = dst_width.div_ceil(4) as usize; | |
| let dst_width = dst_width as usize; | |
| let dst_height = dst_height as usize; | |
| let src_row_width = src_row_width as usize; | |
| let dst_row_width = dst_row_width as usize; | |
| assert!(src_width <= src_row_width); | |
| assert!(dst_width <= dst_row_width); | |
| let min_src_blocks = (src_height - 1) | |
| .checked_mul(src_row_width) | |
| .and_then(|n| n.checked_add(src_width)) | |
| .expect("src dimensions overflow usize"); | |
| let min_src_bytes = | |
| min_src_blocks.checked_mul(ETC1_BLOCK_BYTES).expect("src byte size overflows usize"); | |
| assert!(src_len >= min_src_bytes); | |
| let min_dst_pixels = (dst_height - 1) | |
| .checked_mul(dst_row_width) | |
| .and_then(|n| n.checked_add(dst_width)) | |
| .expect("dst dimensions overflow usize"); | |
| assert!(dst_len >= min_dst_pixels); | |
| } | |
| /// Writes a decoded 4x4 RGBA block into `dst` at `(x, y)`, fast-pathing | |
| /// interior full 4x4 blocks with fixed-size 4-element copies and clipping | |
| /// partial edge blocks to `(dst_width, dst_height)`. | |
| #[inline] | |
| fn write_decoded_block( | |
| dst: &mut [u32], | |
| block: &[[u32; 4]; 4], | |
| x: usize, | |
| y: usize, | |
| dst_width: usize, | |
| dst_height: usize, | |
| dst_row_width: usize, | |
| ) { | |
| if dst_width - x >= 4 && dst_height - y >= 4 { | |
| for (y_in_block, row) in block.iter().enumerate() { | |
| let dst_idx = (y + y_in_block) * dst_row_width + x; | |
| dst[dst_idx..dst_idx + 4].copy_from_slice(row); | |
| } | |
| } else { | |
| let copy_width = (dst_width - x).min(4); | |
| let copy_height = (dst_height - y).min(4); | |
| for (y_in_block, row) in block[..copy_height].iter().enumerate() { | |
| let dst_idx = (y + y_in_block) * dst_row_width + x; | |
| dst[dst_idx..dst_idx + copy_width].copy_from_slice(&row[..copy_width]); | |
| } | |
| } | |
| } | |
| /// Decompress ETC1 blocks to RGBA pixels. | |
| /// | |
| /// - `src` should contain compressed ETC1 blocks. | |
| /// - `dst` will be filled with decompressed RGBA pixels (0xAABBGGRR in u32). | |
| /// - `dst_width` and `dst_height` specify the logical dimensions of `dst` in | |
| /// pixels. If `dst_width` or `dst_height` are not multiples of 4, | |
| /// out-of-bounds pixels in partial edge blocks are discarded. | |
| /// - `src_row_width` and `dst_row_width` specify the in-memory length of each | |
| /// row, in 4x4 blocks and pixels, respectively. | |
| pub fn decompress_etc1( | |
| src: &[u8], | |
| dst: &mut [u32], | |
| dst_width: u32, | |
| dst_height: u32, | |
| src_row_width: u32, | |
| dst_row_width: u32, | |
| ) { | |
| if dst_width == 0 || dst_height == 0 { | |
| return; | |
| } | |
| validate_decompress_buffers( | |
| src.len(), | |
| dst.len(), | |
| dst_width, | |
| dst_height, | |
| src_row_width, | |
| dst_row_width, | |
| ); | |
| let src_height = dst_height.div_ceil(4) as usize; | |
| let src_width = dst_width.div_ceil(4) as usize; | |
| let dst_width = dst_width as usize; | |
| let dst_height = dst_height as usize; | |
| let dst_row_width = dst_row_width as usize; | |
| for src_y in 0..src_height { | |
| let y = src_y * 4; | |
| let row_start_block = src_y * src_row_width as usize; | |
| for src_x in 0..src_width { | |
| let x = src_x * 4; | |
| // The ETC1 specification ("Khronos Data Format Specification v1.1 | |
| // rev 9") defines the 64-bit block data as big endian. | |
| // Reading 8 bytes via `u64::from_be_bytes` avoids any | |
| // 8-byte alignment requirement on `src`. | |
| let block_offset = (row_start_block + src_x) * ETC1_BLOCK_BYTES; | |
| let block_bytes: [u8; ETC1_BLOCK_BYTES] = | |
| src[block_offset..block_offset + ETC1_BLOCK_BYTES].try_into().unwrap(); | |
| let output_rgba_block = decode_etc1_block(u64::from_be_bytes(block_bytes)); | |
| write_decoded_block( | |
| dst, | |
| &output_rgba_block, | |
| x, | |
| y, | |
| dst_width, | |
| dst_height, | |
| dst_row_width, | |
| ); | |
| } | |
| } | |
| } |