| // 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. | |
| use crate::selectors::TABLES; | |
| #[derive(Debug, PartialEq, Eq)] | |
| pub struct BlockMetadata { | |
| pub base: [[i32; 3]; 2], | |
| pub table_idx_1: u32, | |
| pub table_idx_2: u32, | |
| pub flip: bool, | |
| } | |
| // Extract base color in the individual mode. | |
| // Return [base_color1, base_color2] | |
| // See "Khronos Data Format Specification v1.4.0", Section 21, "In the | |
| // 'individual mode'..." | |
| fn parse_individual_base(patch: u64) -> [[i32; 3]; 2] { | |
| // The bit layout for individual mode is: | |
| // 63..60: base color 1 R | |
| // 59..56: base color 2 R | |
| // 55..52: base color 1 G | |
| // 51..48: base color 2 G | |
| // 49..44: base color 1 B | |
| // 43..40: base color 2 B | |
| // The remaining bits are handled in 'parse_block_metadata' | |
| let base = [0, 1].map(|j| { | |
| // i = 0,1,2 maps to [R, G, B] respectively | |
| [0, 1, 2].map(|i| { | |
| let shift = 60 - 4 * (i * 2 + j); | |
| let read_4bit = ((patch >> shift) & 0b1111) as i32; | |
| scale_4bit_to_8bit(read_4bit) | |
| }) | |
| }); | |
| return base; | |
| } | |
| // Expands a 4-bit color component to an 8-bit component | |
| // according to the specification. | |
| // | |
| // We want to scale value in [0, 15] to value in [0, 255]. | |
| // The ideal scaling is (input / 15) * 255 = input * 17. | |
| // The bitwise operation of this function: | |
| // (input << 4) | input | |
| // is equivalent to: | |
| // (input * 2^4) + input = input * 17. | |
| // Thus, the two results are identical. | |
| // | |
| // In addition, the operation of this function is hardware-friendly and | |
| // correctly maps the endpoints: | |
| // - Minimum: 0b_0000(0) becomes 0b_00000000(0). | |
| // - Maximum: 0b_1111(15) becomes 0b_11111111(255). | |
| pub fn scale_4bit_to_8bit(input: i32) -> i32 { | |
| assert!((0..16).contains(&input)); | |
| input << 4 | input | |
| } | |
| // Extract base color in the differential mode. | |
| // Return [base_color1, base_color2], where base_color2 is base_color1 + delta. | |
| // See "Khronos Data Format Specification v1.4.0", Section 21, "In the | |
| // 'differential mode'..." | |
| fn parse_differential_base(patch: u64) -> [[i32; 3]; 2] { | |
| // The bit layout for differential mode is | |
| // 63..59: base color R | |
| // 58..56: color delta R | |
| // 55..51: base color G | |
| // 50..48: color delta G | |
| // 47..43: base color B | |
| // 42..40: color delta B | |
| // The remaining bits are handled in 'parse_block_metadata' | |
| // i = 0,1,2 maps to [R, G, B] respectively | |
| let base_1_5bit = [0, 1, 2].map(|i| { | |
| let shift = 59 - 8 * i; | |
| ((patch >> shift) & 0b11111) as i32 | |
| }); | |
| let base_1 = base_1_5bit.map(scale_5bit_to_8bit); | |
| let base_2 = [0, 1, 2].map(|i| { | |
| let shift = 56 - 8 * i; | |
| let delta = read_delta_bits(((patch >> shift) & 0b111) as u32); | |
| let base_2_5bit = (base_1_5bit[i] + delta).clamp(0, 31); | |
| scale_5bit_to_8bit(base_2_5bit) | |
| }); | |
| [base_1, base_2] | |
| } | |
| // Parses a 3-bit binary string as a two's complement signed integer. | |
| // The range covered is [-4,3]. | |
| pub fn read_delta_bits(input: u32) -> i32 { | |
| assert!(input < 8); | |
| if input >= 4 { | |
| (input as i32) - 8 | |
| } else { | |
| input as i32 | |
| } | |
| } | |
| // Expands a 5-bit color component to an 8-bit component | |
| // according to the specification. | |
| // | |
| // We want to scale value in [0, 31] to value in [0, 255]. | |
| // The ideal scaling is (input / 31) * 255 ~ input * 8.22580... | |
| // The bitwise operation of this function: | |
| // (input <<3) | (input >> 2) | |
| // is equivalent to: | |
| // (input * 2^3) + floor(input / 2^2) ~ input * 8.25. | |
| // The scaling value (8.25) approximates the ideal scaling (8.225...). | |
| // | |
| // In addition, the operation of this function is hardware-friendly and | |
| // correctly maps the endpoints: | |
| // - Minimum: 0b_00000(0) becomes 0b_00000000(0). | |
| // - Maximum: 0b_11111(31) becomes 0b_11111111(255). | |
| pub fn scale_5bit_to_8bit(input: i32) -> i32 { | |
| assert!((0..32).contains(&input)); | |
| input << 3 | input >> 2 | |
| } | |
| // Extract block meta-data from the upper half of 64 bits. | |
| // (See "Khronos Data Format Specification v1.4.0", Section21, Table 138) | |
| // There are 2 ways to extract the base colors. | |
| // One is 'individual' mode (See Table 138 part a), and the other is | |
| // 'differential' mode (See Table 138 part b). | |
| pub fn parse_block_metadata(etc1_block: u64) -> BlockMetadata { | |
| let diff0 = ((etc1_block >> 33) & 0b1) == 0; | |
| let base = | |
| if diff0 { parse_individual_base(etc1_block) } else { parse_differential_base(etc1_block) }; | |
| BlockMetadata { | |
| base, | |
| table_idx_1: ((etc1_block >> 37) & 0b111) as u32, | |
| table_idx_2: ((etc1_block >> 34) & 0b111) as u32, | |
| flip: ((etc1_block >> 32) & 0b1) == 1, | |
| } | |
| } | |
| // The spec specifies the modifier table in the order of -large, -small, +small, | |
| // +large. However, in order to look up with the negative and large bit as-is, | |
| // table needs to be in the order of +small, +large, -small, -large. | |
| // This creates a shuffled table to allow such access. | |
| pub const OPTIMIZED_MODIFIER_TABLE: [[i32; 4]; 8] = generate_modifier_table(); | |
| const fn generate_modifier_table() -> [[i32; 4]; 8] { | |
| let mut table = [[0; 4]; 8]; | |
| let mut i = 0; | |
| // The modifier table provides pairs of [small, large] for each table index. | |
| // The two bits extracted for a pixel select one of four possible deltas. | |
| // | |
| // Selector bits logic: index = (negative << 1) | large | |
| // | Index | Bits (Neg, Large) | Sign | Magnitude | Value | | |
| // |-------|-------------------|------|-----------|--------| | |
| // | 0 | 0, 0 | + | small | +small | | |
| // | 1 | 0, 1 | + | large | +large | | |
| // | 2 | 1, 0 | - | small | -small | | |
| // | 3 | 1, 1 | - | large | -large | | |
| while i < 8 { | |
| let small = TABLES[i][0] as i32; | |
| let large = TABLES[i][1] as i32; | |
| table[i][0] = small; | |
| table[i][1] = large; | |
| table[i][2] = -small; | |
| table[i][3] = -large; | |
| i += 1; | |
| } | |
| table | |
| } | |
| // Applies the modifier (luminance change) to a single pixel and calculates the | |
| // RGBA value. | |
| // - `base`: should be the base color as an [R, G, B]. | |
| // - `subblock_mod`: should be the index for the modifier table. (range 0-7). | |
| // - `pixel_mod': should be the index for the specific delta value. (range 0-3). | |
| // The return value is a u32 packed in the 0xAABBGGRR layout. | |
| pub fn apply_modifier(base: [i32; 3], subblock_mod: u32, pixel_mod: u32) -> u32 { | |
| let modifier_delta = OPTIMIZED_MODIFIER_TABLE[subblock_mod as usize][pixel_mod as usize]; | |
| let mut output_rgba: u32 = 0xFF000000; // Set alpha channel to 255 | |
| for (i, orig_color) in base.into_iter().enumerate() { | |
| let channel_color = (orig_color + modifier_delta).clamp(0, 255) as u32; | |
| // Pack the R,G,B values into a single 32-bit | |
| let shift = i * 8; | |
| output_rgba |= channel_color << shift; | |
| } | |
| return output_rgba; | |
| } | |
| // Expands the lower 16 bits into 32 bits by inserting a 0 bit between each bit. | |
| // Input: 0b_abcd..op | |
| // Output: 0b_0a0b0c..0o0p | |
| fn morton_spread(x: u32) -> u32 { | |
| let x = x & 0x0000FFFF; | |
| let x = (x | (x << 8)) & 0x00FF00FF; | |
| let x = (x | (x << 4)) & 0x0F0F0F0F; | |
| let x = (x | (x << 2)) & 0x33333333; | |
| (x | (x << 1)) & 0x55555555 | |
| } | |
| // Interleaves the upper and lower 16-bit halves into a single 32-bit sequence. | |
| // To calculate the lookup index for each pixel, we need to combine 1 bit from | |
| // the upper half and 1 bit from the lower half. | |
| // Comparison: | |
| // - Naive: 32 shifts + 32 masks (total: 64 ops). | |
| // - Interleaved: 13 ops (shuffle) * 2 + 16 shifts + 16 masks (total: 58 ops). | |
| // -> This results in fewer operations and faster execution. | |
| // This function is for performing the bit shuffling. | |
| // Input: 0b_ab..op_AB..OP | |
| // Output: 0b_aAbB..oOpP | |
| pub fn morton_interleave(input_etc1: u32) -> u32 { | |
| let upper = morton_spread(input_etc1 >> 16); | |
| let lower = morton_spread(input_etc1); | |
| (upper << 1) | lower | |
| } | |
| // Extracts the 2-bit pixel modifier index for the specified coordinates. | |
| fn get_pixel_modifier_index(row: usize, col: usize, morton_interleaved: u32) -> u32 { | |
| let shift = (row + col * 4) * 2; | |
| (morton_interleaved >> shift) & 0b11 | |
| } | |
| // Precomputes the 4 packed 0xAABBGGRR colors for a subblock's base color and | |
| // modifier table index across all 4 2-bit selector indices (0..4). | |
| #[inline] | |
| fn precompute_subblock_palette(base: [i32; 3], table_idx: u32) -> [u32; 4] { | |
| [0, 1, 2, 3].map(|sel| apply_modifier(base, table_idx, sel)) | |
| } | |
| #[allow(clippy::needless_range_loop)] // Range loops seem more readable here. | |
| pub fn decode_etc1_block(input_etc1: u64) -> [[u32; 4]; 4] { | |
| let metadata = parse_block_metadata(input_etc1); | |
| assert!(metadata.table_idx_1 < 8); | |
| assert!(metadata.table_idx_2 < 8); | |
| let morton_interleaved = morton_interleave(input_etc1 as u32); | |
| let palette0 = precompute_subblock_palette(metadata.base[0], metadata.table_idx_1); | |
| let palette1 = precompute_subblock_palette(metadata.base[1], metadata.table_idx_2); | |
| // The output layout is | |
| // [[ a e i m ], | |
| // [ b f j n ], | |
| // [ c g k o ], | |
| // [ d h l p ]]. | |
| let mut output = [[0_u32; 4]; 4]; | |
| // In ETC1, each block can be divided into two subblocks, either vertically | |
| // or horizontally depending on the flip bit. Regardless of the | |
| // direction, the loop is structured so that pixels in the first | |
| // subblock is processed first, then the second. Within each loop, the | |
| // same precomputed 4-color palette is indexed by each pixel's 2-bit | |
| // selector. | |
| if metadata.flip { | |
| // When flip bit = 1, the block is divided into two 4×2 subblocks. | |
| // - Pixels a,e,i,m, b,f,j,n (rows 0, 1) use subblock 1 palette. | |
| // - Pixels c,g,k,o, d,h,l,p (rows 2, 3) use subblock 2 palette. | |
| for row in 0..2 { | |
| for col in 0..4 { | |
| output[row][col] = | |
| palette0[get_pixel_modifier_index(row, col, morton_interleaved) as usize]; | |
| } | |
| } | |
| for row in 2..4 { | |
| for col in 0..4 { | |
| output[row][col] = | |
| palette1[get_pixel_modifier_index(row, col, morton_interleaved) as usize]; | |
| } | |
| } | |
| } else { | |
| // When flip bit = 0, the block is divided into two 2×4 subblocks. | |
| // - Pixels a,b,c,d, e,f,g,h (columns 0, 1) use subblock 1 palette. | |
| // - Pixels i,j,k,l, m,n,o,p (columns 2, 3) use subblock 2 palette. | |
| for row in 0..4 { | |
| for col in 0..2 { | |
| output[row][col] = | |
| palette0[get_pixel_modifier_index(row, col, morton_interleaved) as usize]; | |
| } | |
| for col in 2..4 { | |
| output[row][col] = | |
| palette1[get_pixel_modifier_index(row, col, morton_interleaved) as usize]; | |
| } | |
| } | |
| } | |
| output | |
| } |