Repository: Sewer56/dxt-lossless-transform
Pull request #29: Added: BC3 Block Normalization
Author: login: Sewer56
Created: 2025-05-11T22:25:28Z | Merged: 2025-05-12T03:41:37Z

<!-- Please explain the changes you made -->

<!--
Please, make sure:
- you have read the contributing guidelines:
  https://github.com/Sewer56/dxt-lossless-transform/blob/main/docs/CONTRIBUTING.md
-->


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Introduced configurable normalization for BC3 texture blocks, enabling consistent binary representations for blocks with uniform alpha or solid colors.
  - Added new benchmarking tools to measure BC3 block normalization performance.
  - Added new fuzz tests to validate BC3 normalization correctness.
- **Documentation**
  - Expanded and clarified documentation on BC3 block normalization strategies and their impact on compression.
  - Improved code comments for better code understanding.
- **Bug Fixes**
  - Enhanced fuzz tests to skip unsupported BC2 block configurations.
- **Refactor**
  - Introduced utility methods for checking uniform alpha in decoded blocks.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
index c8792e5a..6ab2b0fa 100644
--- a/fuzz/Cargo.toml
+++ b/fuzz/Cargo.toml
@@ -56,4 +56,11 @@ name = "bc2_normalize"
 path = "fuzz_targets/bc2_normalize.rs"
 test = false
 doc = false
+bench = false
+
+[[bin]]
+name = "bc3_normalize"
+path = "fuzz_targets/bc3_normalize.rs"
+test = false
+doc = false
 bench = false
\ No newline at end of file
diff --git a/fuzz/fuzz_targets/bc2_normalize.rs b/fuzz/fuzz_targets/bc2_normalize.rs
index 7f335c79..c06615a5 100644
--- a/fuzz/fuzz_targets/bc2_normalize.rs
+++ b/fuzz/fuzz_targets/bc2_normalize.rs
@@ -4,6 +4,7 @@
 // to the same pixels as the original blocks.
 
 use dxt_lossless_transform_bc2::{normalize_blocks::normalize_blocks, util::decode_bc2_block};
+use dxt_lossless_transform_common::color_565::Color565;
 use libfuzzer_sys::{arbitrary, fuzz_target};
 
 #[derive(Clone, Debug, arbitrary::Arbitrary)]
@@ -14,6 +15,15 @@ pub struct Bc2Block {
 // Fuzz test for BC2 normalization
 // Tests that normalizing a block preserves its visual appearance when decoded
 fuzz_target!(|block: Bc2Block| {
+    // Skip if c0 <= c1 as that mode is not supported on all GPUs.
+    let c0_raw: u16 = u16::from_le_bytes([block.bytes[8], block.bytes[9]]);
+    let c1_raw: u16 = u16::from_le_bytes([block.bytes[10], block.bytes[11]]);
+    let c0 = Color565::from_raw(c0_raw);
+    let c1 = Color565::from_raw(c1_raw);
+    if !c0.greater_than(&c1) {
+        return;
+    }
+
     // Get a slice to the BC2 block data
     let bc2_block = &block.bytes;
     
diff --git a/fuzz/fuzz_targets/bc3_normalize.rs b/fuzz/fuzz_targets/bc3_normalize.rs
new file mode 100644
index 00000000..c4cd828d
--- /dev/null
+++ b/fuzz/fuzz_targets/bc3_normalize.rs
@@ -0,0 +1,78 @@
+#![no_main]
+
+// This fuzz test validates the BC3 normalizer by checking that the normalized blocks decode
+// to the same pixels as the original blocks.
+
+use dxt_lossless_transform_bc3::{
+    normalize_blocks::{normalize_blocks, AlphaNormalizationMode, ColorNormalizationMode},
+    util::decode_bc3_block,
+};
+use dxt_lossless_transform_common::color_565::Color565;
+use libfuzzer_sys::{arbitrary, fuzz_target};
+
+#[derive(Clone, Debug, arbitrary::Arbitrary)]
+pub struct Bc3Block {
+    pub bytes: [u8; 16],
+}
+
+// Fuzz test for BC3 normalization
+// Tests that normalizing a block preserves its visual appearance when decoded
+fuzz_target!(|block: Bc3Block| {
+    // Skip if c0 <= c1 for the color part, as that mode is not supported on all GPUs for BC3
+    // BC3 color data starts at offset 8
+    let c0_raw: u16 = u16::from_le_bytes([block.bytes[8], block.bytes[9]]);
+    let c1_raw: u16 = u16::from_le_bytes([block.bytes[10], block.bytes[11]]);
+    let c0 = Color565::from_raw(c0_raw);
+    let c1 = Color565::from_raw(c1_raw);
+    if !c0.greater_than(&c1) {
+        return;
+    }
+
+    // Get a slice to the BC3 block data
+    let bc3_block = &block.bytes;
+
+    // Save the original block for later comparison
+    let original_decoded = unsafe { decode_bc3_block(bc3_block.as_ptr()) };
+
+    // Test different combinations of normalization modes
+    let alpha_modes = [
+        AlphaNormalizationMode::UniformAlphaZeroIndices,
+        AlphaNormalizationMode::OpaqueFillAll,
+        AlphaNormalizationMode::OpaqueZeroAlphaMaxIndices,
+    ];
+
+    let color_modes = [
+        ColorNormalizationMode::Color0Only,
+        ColorNormalizationMode::ReplicateColor,
+    ];
+
+    for &alpha_mode in &alpha_modes {
+        for &color_mode in &color_modes {
+            // Create a buffer for the normalized block
+            let mut normalized_block = [0u8; 16];
+
+            // Normalize the block with the current modes
+            unsafe {
+                normalize_blocks(
+                    bc3_block.as_ptr(),
+                    normalized_block.as_mut_ptr(),
+                    16, // Size of BC3 block in bytes
+                    alpha_mode,
+                    color_mode,
+                );
+            }
+
+            // Decode the normalized block
+            let normalized_decoded = unsafe { decode_bc3_block(normalized_block.as_ptr()) };
+
+            // Compare the two decoded blocks - they should produce the same visual output
+            assert_eq!(
+                original_decoded, normalized_decoded,
+                "Normalized block doesn't decode to the same pixels as the original block\n\
+                 Alpha mode: {alpha_mode:?}, Color mode: {color_mode:?}\n\
+                 Original block: {bc3_block:?}\n\
+                 Normalized block: {normalized_block:?}"
+            );
+        }
+    }
+});
diff --git a/projects/dxt-lossless-transform-bc3/Cargo.toml b/projects/dxt-lossless-transform-bc3/Cargo.toml
index 619147fc..bd6485ff 100644
--- a/projects/dxt-lossless-transform-bc3/Cargo.toml
+++ b/projects/dxt-lossless-transform-bc3/Cargo.toml
@@ -21,6 +21,7 @@ nightly = []
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 [dependencies]
 dxt-lossless-transform-common = { path = "../dxt-lossless-transform-common" }
+likely_stable = "0.1.3"
 
 [dev-dependencies]
 criterion = "0.5.1"
@@ -45,3 +46,8 @@ harness = false
 name = "block_decode"
 path = "benches/block_decode/main.rs"
 harness = false
+
+[[bench]]
+name = "normalize_blocks"
+path = "benches/normalize_blocks/main.rs"
+harness = false
diff --git a/projects/dxt-lossless-transform-bc3/README.MD b/projects/dxt-lossless-transform-bc3/README.MD
index 19a83cfc..86160d66 100644
--- a/projects/dxt-lossless-transform-bc3/README.MD
+++ b/projects/dxt-lossless-transform-bc3/README.MD
@@ -53,11 +53,56 @@ Separates colours and indices into continuous streams:
 +-------+-------+-------+     +-------+
 ```
 
-### Making Solid Blocks Represented the Same Across Texture
+### Making Single Colour Blocks Represented the Same Across Texture
 
-BC3 can represent solid color blocks in multiple ways, which hinders compression.
+BC3 can represent single color blocks in multiple ways, which hinders compression.
 
-- TODO
+Recall BC3 block format:
+
+```text
+Address: 0       2          8   8        12        16
+         +-------+----------+   +---------+---------+
+Data:    | A0-A1 | AI0-AI15 |   | C00-C01 | I00-I15 |
+         +-------+----------+   +---------+---------+
+```
+
+Two bytes of Alpha (8 bits each) followed by 6 bytes of indices (3 bits each).
+The indices are used to interpolate the alpha values.
+
+There are 2 steps to normalizing here:
+
+#### Normalizing the Alphas
+
+Sometimes all items within a block have the same alpha component, for example, all pixels in a block
+are fully opaque (`0xFF`), or are semi transparent (`0xD0`). 
+
+When a block is fully opaque, there are multiple well compressible representations which can be used:
+
+- All 8 bytes set to `0xFF` (`0xFFFFFFFF 0xFFFFFFFF`).
+  - Because `A0` <= `A1`, index 0xFF is hardcoded to opaque on decoder side.
+- Zero alphas but indices set to `0xFF` (`0x0000FFFF 0xFFFFFFFF`).
+
+For all other values (including `0xFF`), the following representation is possible:
+
+- `A0` set to `0xFF` and everything else set to `0x00` (`0xFF000000 0x00000000`).
+  - Everything uses colour from the first item.
+
+Which means there's `3` modes for fully opaque, and single `1` mode for everything else.
+
+#### Normalizing the Colours
+
+(This is the same as BC2)
+
+Sometimes there is a clean conversion between `8888` and `565` colour, meaning that it's possible
+to represent a colour using only 1 out of the 2 colour components, i.e. only `C0` or `C1`.
+
+When an entire block is solid, and a clean conversion between `8888` and `565` is possible, we set
+colour `C0` to the colour of the block, and then set `C1`, `I0-I3` all to `0x00`. This results in a
+nice repetition of `0x00` across 6 bytes. In some cases, it's beneficial to replicate
+the colour across `C0` and `C1`. We determine based on whichever performs better by compressing the data.
+
+Note: With BC2, BC3 it's important that we put the colour in `C0` because the 'alternate alpha mode' of
+BC1 where `c0 <= c1` is unsupported; it leads to undefined behaviour on some GPUs.
 
 ### Decorrelating Colours
 
diff --git a/projects/dxt-lossless-transform-bc3/benches/normalize_blocks/main.rs b/projects/dxt-lossless-transform-bc3/benches/normalize_blocks/main.rs
new file mode 100644
index 00000000..d8c87c96
--- /dev/null
+++ b/projects/dxt-lossless-transform-bc3/benches/normalize_blocks/main.rs
@@ -0,0 +1,128 @@
+use core::{alloc::Layout, ptr::copy_nonoverlapping};
+use criterion::{criterion_group, criterion_main, Criterion};
+use dxt_lossless_transform_bc3::normalize_blocks::{
+    normalize_blocks, AlphaNormalizationMode, ColorNormalizationMode,
+};
+use safe_allocator_api::RawAlloc;
+use std::fs;
+
+#[cfg(not(target_os = "windows"))]
+use pprof::criterion::{Output, PProfProfiler};
+
+// Path to the BC3 file to benchmark with
+// You may need to update this path to point to a valid BC3 texture file on your system
+const TEST_FILE_PATH: &str =
+    "/home/sewer/Temp/texture-stuff/bc3-raw/202x-architecture-10.01/whiterun/wrcastlecarpets01.dds";
+
+pub(crate) fn allocate_align_64(num_bytes: usize) -> RawAlloc {
+    let layout = Layout::from_size_align(num_bytes, 64).unwrap();
+    RawAlloc::new(layout).unwrap()
+}
+
+#[allow(clippy::needless_range_loop)]
+fn criterion_benchmark(c: &mut Criterion) {
+    let mut group = c.benchmark_group("BC3 Normalize Blocks");
+
+    // Try to load the test file
+    let file_data = match fs::read(TEST_FILE_PATH) {
+        Ok(data) => {
+            println!("Successfully loaded test file: {TEST_FILE_PATH}");
+            data
+        }
+        Err(e) => {
+            println!("Warning: Could not load test file '{TEST_FILE_PATH}': {e}");
+            println!("To run this benchmark, you need a valid BC3 texture file.");
+            println!("Please update the TEST_FILE_PATH constant to point to a valid BC3 file or create a sample BC3 file.");
+            println!("Alternatively, we'll create a synthetic BC3 file for benchmarking.");
+
+            // Create a synthetic file with 1024 identical BC3 blocks for benchmarking
+            // BC3 block is 16 bytes (8 for alpha, 4 for colors, 4 for indices)
+            let block_size = 16;
+            let num_blocks = 1024;
+            let total_size = block_size * num_blocks;
+
+            // Create a sample BC3 block with solid red color
+            let mut sample_block = [0u8; 16];
+
+            // Alpha part (first 8 bytes) - all fully opaque (255)
+            for x in 0..8 {
+                sample_block[x] = 0xFF;
+            }
+
+            // Color endpoints (RGB565)
+            // Red = 0xF800 in RGB565 (little endian: [0x00, 0xF8])
+            sample_block[8] = 0x00;
+            sample_block[9] = 0xF8;
+            sample_block[10] = 0x00; // Second color not used
+            sample_block[11] = 0x00;
+
+            // Indices (all 0)
+            sample_block[12] = 0;
+            sample_block[13] = 0;
+            sample_block[14] = 0;
+            sample_block[15] = 0;
+
+            // Create the synthetic file by repeating the sample block
+            let mut synthetic_data = Vec::with_capacity(total_size);
+            for _ in 0..num_blocks {
+                synthetic_data.extend_from_slice(&sample_block);
+            }
+
+            println!(
+                "Created synthetic BC3 file with {} blocks ({} bytes) for benchmarking.",
+                num_blocks,
+                synthetic_data.len()
+            );
+            synthetic_data
+        }
+    };
+
+    // Ensure we have a file size that's a multiple of 16 (BC3 block size)
+    let file_size = file_data.len();
+
+    // Allocate memory for input and output
+    let mut input = allocate_align_64(file_size);
+    let mut output = allocate_align_64(file_size);
+
+    // Copy file data to input buffer
+    unsafe {
+        copy_nonoverlapping(file_data.as_ptr(), input.as_mut_ptr(), file_size);
+    }
+
+    let input_ptr = input.as_ptr();
+    let output_ptr = output.as_mut_ptr();
+
+    group.throughput(criterion::Throughput::Bytes(file_size as u64));
+
+    // Benchmark the normalize_blocks function
+    group.bench_function("normalize_blocks", |b| {
+        b.iter(|| unsafe {
+            normalize_blocks(
+                input_ptr,
+                output_ptr,
+                file_size,
+                // The mode doesn't really matter, the bottleneck isn't in the write here.
+                AlphaNormalizationMode::UniformAlphaZeroIndices,
+                ColorNormalizationMode::Color0Only,
+            );
+        })
+    });
+
+    group.finish();
+}
+
+#[cfg(not(target_os = "windows"))]
+criterion_group! {
+    name = benches;
+    config = Criterion::default().with_profiler(PProfProfiler::new(100, Output::Flamegraph(None)));
+    targets = criterion_benchmark
+}
+
+#[cfg(target_os = "windows")]
+criterion_group! {
+    name = benches;
+    config = Criterion::default();
+    targets = criterion_benchmark
+}
+
+criterion_main!(benches);
diff --git a/projects/dxt-lossless-transform-bc3/src/lib.rs b/projects/dxt-lossless-transform-bc3/src/lib.rs
index 6fa94063..20ed2646 100644
--- a/projects/dxt-lossless-transform-bc3/src/lib.rs
+++ b/projects/dxt-lossless-transform-bc3/src/lib.rs
@@ -3,6 +3,7 @@
 #![cfg_attr(feature = "nightly", feature(avx512_target_feature))]
 #![cfg_attr(feature = "nightly", feature(stdarch_x86_avx512))]
 
+pub mod normalize_blocks;
 pub mod split_blocks;
 pub mod util;
 
diff --git a/projects/dxt-lossless-transform-bc3/src/normalize_blocks/mod.rs b/projects/dxt-lossless-transform-bc3/src/normalize_blocks/mod.rs
new file mode 100644
index 00000000..cfd654c0
--- /dev/null
+++ b/projects/dxt-lossless-transform-bc3/src/normalize_blocks/mod.rs
@@ -0,0 +1,877 @@
+//! # Block Normalization Process
+//!
+//! This module contains the code used to normalize BC3 blocks to improve compression ratio
+//! by making solid color blocks and alpha values have consistent representations.
+//!
+//! ## BC3 Block Format
+//!
+//! First, let's recall the BC3 block format:
+//!
+//! ```text
+//! Address: 0       2        8       12      16
+//!          +-------+--------+  +-------+--------+
+//! Data:    | A0-A1 |AI0-AI15|  | C0-C1 | I0-I15 |
+//!          +-------+--------+  +-------+--------+
+//! ```
+//!
+//! Where:
+//! - `A0-A1` are two alpha endpoints (8-bit each)
+//! - `AI0-AI15` are 6 bytes containing sixteen 3-bit alpha indices
+//! - `C0-C1` are 16-bit RGB565 color values (2 bytes each)
+//! - `I0-I15` are 4 bytes containing sixteen 2-bit color indices
+//!
+//! ## Normalization Rules
+//!
+//! The normalization process applies different rules for alpha and color components:
+//!
+//! ### Alpha Normalization
+//!
+//! When an entire block has uniform alpha, several representations are possible:
+//!
+//! For fully opaque blocks (`0xFF`):
+//! - All 8 bytes set to `0xFF` i.e. (`0xFFFFFFFF 0xFFFFFFFF`).
+//!   - Because `A0` <= `A1`, index `0xFF` is hardcoded to opaque on the decoder side.
+//! - Zero alphas but indices set to `0xFF` i.e. (`0x0000FFFF 0xFFFFFFFF`).
+//!
+//! For all other values (including `0xFF`):
+//! - `A0` set to the alpha value, everything else to `0x00` i.e. (`0xFF000000 0x00000000`).
+//!   - Everything uses the alpha value from the first endpoint.
+//!
+//! ### Color Normalization
+//!
+//! For solid color blocks with clean RGB565 conversion:
+//! - Set color in `C0`, zeroes in `C1` and indices
+//!   - This results in a nice repetition of `0x00` across 6 bytes
+//! - Or replicate color in both `C0` and `C1`, zeroes in indices
+//!   - In some cases, this performs better in compression
+//!
+//! Note: With BC3, it's important that we put the color in `C0` because the 'alternate alpha mode' of
+//! BC1 where `c0 <= c1` is unsupported; it leads to undefined behavior on some GPUs.
+//!
+//! ## Implementation Details
+//!
+//! The normalization process:
+//!
+//! 1. Decodes the block to get all 16 pixels with their colors and alpha values
+//! 2. Checks for uniform alpha and solid colors
+//! 3. Applies appropriate normalization based on the selected modes
+//! 4. Writes the normalized block to the output
+
+use crate::util::decode_bc3_block;
+use core::ptr::copy_nonoverlapping;
+use dxt_lossless_transform_common::color_8888::Color8888;
+use likely_stable::unlikely;
+
+/// Reads an input of BC3 blocks from `input_ptr` and writes the normalized blocks to `output_ptr`.
+///
+/// # Parameters
+///
+/// - `input_ptr`: A pointer to the input data (input BC3 blocks)
+/// - `output_ptr`: A pointer to the output data (output BC3 blocks)
+/// - `len`: The length of the input data in bytes
+/// - `alpha_mode`: How to normalize alpha values
+/// - `color_mode`: How to normalize color values
+///
+/// # Safety
+///
+/// - input_ptr must be valid for reads of len bytes
+/// - output_ptr must be valid for writes of len bytes
+/// - len must be divisible by 16 (BC3 block size)
+///
+/// # Remarks
+///
+/// This function identifies and normalizes BC3 blocks based on their content:
+/// - Blocks with uniform alpha are normalized according to the alpha_mode
+/// - Solid color blocks are normalized according to the color_mode
+/// - Other blocks are preserved as-is
+///
+/// Normalization improves compression ratios by ensuring that similar visual blocks
+/// have identical binary representations, reducing entropy in the data.
+#[inline]
+pub unsafe fn normalize_blocks(
+    input_ptr: *const u8,
+    output_ptr: *mut u8,
+    len: usize,
+    alpha_mode: AlphaNormalizationMode,
+    color_mode: ColorNormalizationMode,
+) {
+    debug_assert!(len % 16 == 0);
+
+    // Skip normalization if both modes are None
+    if alpha_mode == AlphaNormalizationMode::None && color_mode == ColorNormalizationMode::None {
+        copy_nonoverlapping(input_ptr, output_ptr, len);
+        return;
+    }
+
+    // Calculate pointers to current block
+    let mut src_block_ptr = input_ptr;
+    let mut dst_block_ptr = output_ptr;
+    let src_end_ptr = input_ptr.add(len);
+
+    // Process each block
+    while src_block_ptr < src_end_ptr {
+        // Decode the block to analyze its content
+        let decoded_block = decode_bc3_block(src_block_ptr);
+
+        // Check for uniform alpha
+        let has_uniform_alpha = decoded_block.has_identical_alpha();
+
+        // Check for solid color (ignoring alpha)
+        let has_solid_color = decoded_block.has_identical_pixels_ignore_alpha();
+
+        if (alpha_mode != AlphaNormalizationMode::None && has_uniform_alpha)
+            || (color_mode != ColorNormalizationMode::None && has_solid_color)
+        {
+            // Handle alpha normalization
+            if has_uniform_alpha {
+                let alpha_value = decoded_block.pixels[0].a;
+                normalize_alpha(src_block_ptr, dst_block_ptr, alpha_value, alpha_mode);
+            } else {
+                // Copy alpha data as-is (first 8 bytes)
+                copy_nonoverlapping(src_block_ptr, dst_block_ptr, 8);
+            }
+
+            // Handle color normalization
+            if color_mode != ColorNormalizationMode::None && has_solid_color {
+                // Get the first pixel (they all have the same color)
+                let pixel = decoded_block.pixels[0];
+
+                // Convert the color to RGB565
+                let color565 = pixel.to_color_565();
+
+                // Check if color can be round-tripped cleanly through RGB565
+                let color8888 = color565.to_color_8888();
+                let pixel_ignore_alpha = Color8888::new(pixel.r, pixel.g, pixel.b, 255);
+                let color8888_ignore_alpha =
+                    Color8888::new(color8888.r, color8888.g, color8888.b, 255);
+
+                if unlikely(color8888_ignore_alpha == pixel_ignore_alpha) {
+                    // Can be normalized
+                    normalize_color(color565.raw_value(), dst_block_ptr, color_mode);
+                } else {
+                    // Cannot normalize, copy color part as-is
+                    copy_nonoverlapping(src_block_ptr.add(8), dst_block_ptr.add(8), 8);
+                }
+            } else {
+                // Copy color data as-is (last 8 bytes)
+                copy_nonoverlapping(src_block_ptr.add(8), dst_block_ptr.add(8), 8);
+            }
+        } else {
+            // Block doesn't need normalization, copy as-is
+            copy_nonoverlapping(src_block_ptr, dst_block_ptr, 16);
+        }
+
+        // Move to the next block
+        src_block_ptr = src_block_ptr.add(16);
+        dst_block_ptr = dst_block_ptr.add(16);
+    }
+}
+
+/// Defines how alpha values should be normalized for BC3 blocks
+///
+/// BC3 blocks can represent uniform alpha values in multiple ways. This enum
+/// defines the strategies for normalizing these representations to improve compression.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum AlphaNormalizationMode {
+    /// No alpha normalization, preserves original alpha data
+    None,
+
+    /// For uniform alpha, set `A0` to the alpha value, `A1` to zero, and indices to zero
+    /// This creates a pattern of `alpha,0,0,0,0,0,0,0` for the alpha component
+    UniformAlphaZeroIndices,
+
+    /// For fully opaque, use all 0xFF bytes in the alpha component
+    /// Creates a pattern of `0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF`.
+    ///
+    /// If the alpha value is not fully opaque, this mode will use [`AlphaNormalizationMode::UniformAlphaZeroIndices`]
+    OpaqueFillAll,
+
+    /// For fully opaque, use zero alphas but 0xFF indices
+    /// Creates a pattern of `0,0,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF`
+    ///
+    /// If the alpha value is not fully opaque, this mode will use [`AlphaNormalizationMode::UniformAlphaZeroIndices`]
+    OpaqueZeroAlphaMaxIndices,
+}
+
+/// Defines how colors should be normalized for BC3 blocks
+///
+/// BC3 blocks can represent solid colors in multiple ways. This enum
+/// defines the strategies for normalizing these representations to improve compression.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum ColorNormalizationMode {
+    /// No color normalization, preserves original color data
+    None,
+
+    /// For solid color blocks, put color in C0, zeroes in C1 and indices
+    /// Creates a pattern of `color,0,0,0,0,0,0,0` for the color component
+    /// This results in a nice repetition of `0x00` across 6 bytes
+    Color0Only,
+
+    /// For solid color blocks, replicate color in both C0 and C1, zeroes in indices
+    /// Creates a pattern of `color,color,0,0,0,0` for the color component
+    /// In some cases, this performs better in compression.
+    ReplicateColor,
+}
+
+/// Normalizes the alpha component of a BC3 block
+///
+/// Applies the selected normalization mode to the alpha component of a BC3 block.
+/// This is particularly effective for blocks with uniform alpha values.
+///
+/// # Parameters
+///
+/// - `src_block_ptr`: Pointer to the source BC3 block
+/// - `dst_block_ptr`: Pointer to the destination where the normalized block will be written
+/// - `alpha_value`: The uniform alpha value found in the block (0-255)
+/// - `mode`: The alpha normalization mode to use
+///
+/// # Safety
+///
+/// This is an unsafe function that requires:
+/// - Valid pointers to source and destination blocks with at least 8 bytes accessible
+/// - Alpha value must be the actual uniform alpha found in the block
+#[inline]
+unsafe fn normalize_alpha(
+    src_block_ptr: *const u8,
+    dst_block_ptr: *mut u8,
+    alpha_value: u8,
+    mode: AlphaNormalizationMode,
+) {
+    match mode {
+        AlphaNormalizationMode::None => {
+            // Copy alpha data as-is (first 8 bytes)
+            copy_nonoverlapping(src_block_ptr, dst_block_ptr, 8);
+        }
+        AlphaNormalizationMode::UniformAlphaZeroIndices => {
+            // Set A0 to the alpha value, everything else to 0
+            *dst_block_ptr = alpha_value;
+            *dst_block_ptr.add(1) = 0;
+
+            // Zero all index bytes
+            for x in 2..8 {
+                *dst_block_ptr.add(x) = 0;
+            }
+        }
+        AlphaNormalizationMode::OpaqueFillAll => {
+            if alpha_value == 255 {
+                // Fill all alpha bytes with 0xFF
+                for x in 0..8 {
+                    *dst_block_ptr.add(x) = 0xFF;
+                }
+            } else {
+                // For non-opaque, use the same approach as UniformAlphaZeroIndices
+                normalize_alpha(
+                    src_block_ptr,
+                    dst_block_ptr,
+                    alpha_value,
+                    AlphaNormalizationMode::UniformAlphaZeroIndices,
+                );
+            }
+        }
+        AlphaNormalizationMode::OpaqueZeroAlphaMaxIndices => {
+            if alpha_value == 255 {
+                // Set alpha endpoints to 0
+                *dst_block_ptr = 0;
+                *dst_block_ptr.add(1) = 0;
+
+                // Set all indices to max value (0xFF)
+                for x in 2..8 {
+                    *dst_block_ptr.add(x) = 0xFF;
+                }
+            } else {
+                // For non-opaque, use the same approach as UniformAlphaZeroIndices
+                normalize_alpha(
+                    src_block_ptr,
+                    dst_block_ptr,
+                    alpha_value,
+                    AlphaNormalizationMode::UniformAlphaZeroIndices,
+                );
+            }
+        }
+    }
+}
+
+/// Normalizes the color component of a BC3 block
+///
+/// Applies the selected normalization mode to the color component of a BC3 block.
+/// This is particularly effective for blocks with solid colors that have a clean
+/// conversion between 8888 and 565 color formats.
+///
+/// # Parameters
+///
+/// - `color565`: The RGB565 color value to use for normalization
+/// - `dst_block_ptr`: Pointer to the destination block where normalized color will be written
+/// - `mode`: The color normalization mode to use
+///
+/// # Safety
+///
+/// This is an unsafe function that requires:
+/// - Valid pointer to destination block with at least 8 bytes accessible at offset 8
+/// - color565 must be the actual RGB565 value corresponding to the solid color
+#[inline]
+unsafe fn normalize_color(color565: u16, dst_block_ptr: *mut u8, mode: ColorNormalizationMode) {
+    let color_ptr = dst_block_ptr.add(8);
+    let color_bytes = color565.to_le_bytes();
+
+    match mode {
+        ColorNormalizationMode::None => {
+            // Do nothing - color component was already copied elsewhere
+        }
+        ColorNormalizationMode::Color0Only => {
+            // Write Color0 (the solid color)
+            *color_ptr = color_bytes[0];
+            *color_ptr.add(1) = color_bytes[1];
+
+            // Write Color1 = 0
+            *color_ptr.add(2) = 0;
+            *color_ptr.add(3) = 0;
+
+            // Write indices = 0
+            *color_ptr.add(4) = 0;
+            *color_ptr.add(5) = 0;
+            *color_ptr.add(6) = 0;
+            *color_ptr.add(7) = 0;
+        }
+        ColorNormalizationMode::ReplicateColor => {
+            // Write Color0 (the solid color)
+            *color_ptr = color_bytes[0];
+            *color_ptr.add(1) = color_bytes[1];
+
+            // Write Color1 = same color
+            *color_ptr.add(2) = color_bytes[0];
+            *color_ptr.add(3) = color_bytes[1];
+
+            // Write indices = 0
+            *color_ptr.add(4) = 0;
+            *color_ptr.add(5) = 0;
+            *color_ptr.add(6) = 0;
+            *color_ptr.add(7) = 0;
+        }
+    }
+}
+
+#[cfg(test)]
+#[allow(clippy::needless_range_loop)]
+mod tests {
+    use rstest::rstest;
+
+    use super::*;
+
+    /// Test normalizing a solid color block with uniform alpha
+    #[rstest]
+    #[case(AlphaNormalizationMode::None, ColorNormalizationMode::Color0Only)]
+    #[case(AlphaNormalizationMode::None, ColorNormalizationMode::ReplicateColor)]
+    #[case(
+        AlphaNormalizationMode::UniformAlphaZeroIndices,
+        ColorNormalizationMode::Color0Only
+    )]
+    #[case(AlphaNormalizationMode::OpaqueFillAll, ColorNormalizationMode::None)]
+    #[case(
+        AlphaNormalizationMode::OpaqueZeroAlphaMaxIndices,
+        ColorNormalizationMode::None
+    )]
+    #[case(
+        AlphaNormalizationMode::OpaqueFillAll,
+        ColorNormalizationMode::Color0Only
+    )]
+    #[case(
+        AlphaNormalizationMode::OpaqueZeroAlphaMaxIndices,
+        ColorNormalizationMode::ReplicateColor
+    )]
+    fn can_normalize_block_opaque_alpha_single_colour(
+        #[case] alpha_mode: AlphaNormalizationMode,
+        #[case] color_mode: ColorNormalizationMode,
+    ) {
+        // Red in RGB565: (31, 0, 0) -> 0xF800
+        // This cleanly round trips into (255, 0, 0) and back to (31, 0, 0).
+        let red565 = 0xF800u16.to_le_bytes(); // Little endian: [0x00, 0xF8]
+
+        // This creates a BC3 block with the following characteristics:
+        // - Alpha endpoints (A0=A1=0xFF)
+        // - Alpha indices all point to A0
+        // - Color0 = Red (RGB565)
+        // - Color1 = Another color (doesn't matter)
+        // - Indices = All pointing to Color0 (all 0b00)
+        let mut block = [0u8; 16];
+
+        // Set alpha endpoints to fully opaque
+        block[0] = 0xFF; // A0
+        block[1] = 0xFF; // A1
+
+        // Set alpha indices to point to A0
+        for x in 2..8 {
+            block[x] = 0; // All indices point to A0
+        }
+
+        // Set Color0 to red
+        block[8] = red565[0];
+        block[9] = red565[1];
+
+        // Set Color1 to another color (doesn't matter)
+        block[10] = 0x12;
+        block[11] = 0x34;
+
+        // Set indices to all point to Color0
+        for x in 12..16 {
+            block[x] = 0; // All indices point to Color0
+        }
+
+        // Create output buffer
+        let mut output = [0u8; 16];
+
+        // Normalize the block
+        unsafe {
+            normalize_blocks(
+                block.as_ptr(),
+                output.as_mut_ptr(),
+                16,
+                alpha_mode,
+                color_mode,
+            );
+        }
+
+        // Verify normalization
+        // For alpha
+        match alpha_mode {
+            AlphaNormalizationMode::None => {
+                // Alpha part should be unchanged
+                assert_eq!(output[0], 0xFF);
+                assert_eq!(output[1], 0xFF);
+                for x in 2..8 {
+                    assert_eq!(output[x], 0);
+                }
+            }
+            AlphaNormalizationMode::UniformAlphaZeroIndices => {
+                // A0 set to alpha value, rest zero
+                assert_eq!(output[0], 0xFF);
+                assert_eq!(output[1], 0);
+                for x in 2..8 {
+                    assert_eq!(output[x], 0);
+                }
+            }
+            AlphaNormalizationMode::OpaqueFillAll => {
+                // For fully opaque, all alpha bytes should be 0xFF
+                for x in 0..8 {
+                    assert_eq!(output[x], 0xFF);
+                }
+            }
+            AlphaNormalizationMode::OpaqueZeroAlphaMaxIndices => {
+                // For fully opaque, alpha endpoints should be 0, indices should be 0xFF
+                assert_eq!(output[0], 0);
+                assert_eq!(output[1], 0);
+                for x in 2..8 {
+                    assert_eq!(output[x], 0xFF);
+                }
+            }
+        }
+
+        // For color
+        match color_mode {
+            ColorNormalizationMode::Color0Only => {
+                // Color0 = red, Color1 = 0, indices = 0
+                assert_eq!(output[8], red565[0]);
+                assert_eq!(output[9], red565[1]);
+                assert_eq!(output[10], 0);
+                assert_eq!(output[11], 0);
+                for x in 12..16 {
+                    assert_eq!(output[x], 0);
+                }
+            }
+            ColorNormalizationMode::ReplicateColor => {
+                // Color0 = Color1 = red, indices = 0
+                assert_eq!(output[8], red565[0]);
+                assert_eq!(output[9], red565[1]);
+                assert_eq!(output[10], red565[0]);
+                assert_eq!(output[11], red565[1]);
+                for x in 12..16 {
+                    assert_eq!(output[x], 0);
+                }
+            }
+            _ => {}
+        }
+
+        // Decode the normalized block to verify it still represents the same pixels
+        let decoded = unsafe { decode_bc3_block(output.as_ptr()) };
+
+        // Check all pixels have the correct color and alpha
+        for x in 0..16 {
+            assert_eq!(decoded.pixels[x].r, 255); // Red
+            assert_eq!(decoded.pixels[x].g, 0);
+            assert_eq!(decoded.pixels[x].b, 0);
+            assert_eq!(decoded.pixels[x].a, 255); // Fully opaque
+        }
+    }
+
+    /// Test normalizing a block with uniform alpha but mixed colors
+    #[rstest]
+    #[case(AlphaNormalizationMode::UniformAlphaZeroIndices)]
+    #[case(AlphaNormalizationMode::OpaqueFillAll)]
+    fn can_normalize_uniform_alpha_mixed_colors(#[case] alpha_mode: AlphaNormalizationMode) {
+        // Create a BC3 block with uniform alpha but mixed colors
+        let mut block = [0u8; 16];
+
+        // Set alpha endpoints to fully opaque
+        block[0] = 0xFF; // A0
+        block[1] = 0xFF; // A1
+
+        // Set alpha indices to point to A0
+        for x in 2..8 {
+            block[x] = 0; // All indices point to A0
+        }
+
+        // Set Color0 and Color1 to different colors
+        block[8] = 0x00;
+        block[9] = 0xF8; // Red
+        block[10] = 0xE0;
+        block[11] = 0x07; // Green
+
+        // Set mixed indices (use both colors)
+        block[12] = 0x55; // Alternating 01 01 01 01
+        block[13] = 0x55;
+        block[14] = 0x55;
+        block[15] = 0x55;
+
+        // Create output buffer
+        let mut output = [0u8; 16];
+
+        // Normalize the block
+        unsafe {
+            normalize_blocks(
+                block.as_ptr(),
+                output.as_mut_ptr(),
+                16,
+                alpha_mode,
+                ColorNormalizationMode::None,
+            );
+        }
+
+        // Verify alpha normalization
+        match alpha_mode {
+            AlphaNormalizationMode::UniformAlphaZeroIndices => {
+                // A0 set to alpha value, rest zero
+                assert_eq!(output[0], 0xFF);
+                assert_eq!(output[1], 0);
+                for x in 2..8 {
+                    assert_eq!(output[x], 0);
+                }
+            }
+            AlphaNormalizationMode::OpaqueFillAll => {
+                // All alpha bytes set to 0xFF
+                for x in 0..8 {
+                    assert_eq!(output[x], 0xFF);
+                }
+            }
+            _ => {}
+        }
+
+        // Color part should be unchanged
+        assert_eq!(output[8], block[8]);
+        assert_eq!(output[9], block[9]);
+        assert_eq!(output[10], block[10]);
+        assert_eq!(output[11], block[11]);
+        assert_eq!(output[12], block[12]);
+        assert_eq!(output[13], block[13]);
+        assert_eq!(output[14], block[14]);
+        assert_eq!(output[15], block[15]);
+
+        // Decode the normalized block to verify it still represents the same visual data
+        let decoded = unsafe { decode_bc3_block(output.as_ptr()) };
+
+        // All pixels should have alpha = 255
+        for x in 0..16 {
+            assert_eq!(decoded.pixels[x].a, 255);
+        }
+    }
+
+    /// Test normalizing a block with mixed alpha and solid color
+    #[rstest]
+    #[case(ColorNormalizationMode::Color0Only)]
+    #[case(ColorNormalizationMode::ReplicateColor)]
+    fn can_normalize_mixed_alpha_solid_color(#[case] color_mode: ColorNormalizationMode) {
+        // Create a BC3 block with varying alpha but solid color
+        let mut block = [0u8; 16];
+
+        // Set different alpha endpoints
+        block[0] = 0xFF; // A0
+        block[1] = 0x80; // A1
+
+        // Set mixed alpha indices
+        for x in 2..8 {
+            block[x] = 0x55; // Mixed indices
+        }
+
+        // Set Color0 and Color1 to the same color (red)
+        let red565 = 0xF800u16.to_le_bytes();
+        block[8] = red565[0];
+        block[9] = red565[1];
+        block[10] = 0x12; // Different value for color1
+        block[11] = 0x34;
+
+        // Set all indices to point to Color0
+        for x in 12..16 {
+            block[x] = 0;
+        }
+
+        // Create output buffer
+        let mut output = [0u8; 16];
+
+        // Normalize the block
+        unsafe {
+            normalize_blocks(
+                block.as_ptr(),
+                output.as_mut_ptr(),
+                16,
+                AlphaNormalizationMode::OpaqueFillAll,
+                color_mode,
+            );
+        }
+
+        // Alpha part should be unchanged regardless of option
+        for x in 0..8 {
+            assert_eq!(output[x], block[x]);
+        }
+
+        // Verify color normalization
+        match color_mode {
+            ColorNormalizationMode::Color0Only => {
+                // Color0 = red, Color1 = 0, indices = 0
+                assert_eq!(output[8], red565[0]);
+                assert_eq!(output[9], red565[1]);
+                assert_eq!(output[10], 0);
+                assert_eq!(output[11], 0);
+                for x in 12..16 {
+                    assert_eq!(output[x], 0);
+                }
+            }
+            ColorNormalizationMode::ReplicateColor => {
+                // Color0 = Color1 = red, indices = 0
+                assert_eq!(output[8], red565[0]);
+                assert_eq!(output[9], red565[1]);
+                assert_eq!(output[10], red565[0]);
+                assert_eq!(output[11], red565[1]);
+                for x in 12..16 {
+                    assert_eq!(output[x], 0);
+                }
+            }
+            _ => {}
+        }
+    }
+
+    /// Test the OpaqueZeroAlphaMaxIndices alpha mode
+    #[test]
+    fn can_normalize_with_opaque_zero_alpha_max_indices() {
+        // Create a BC3 block with fully opaque alpha
+        let mut block = [0u8; 16];
+
+        // Set alpha endpoints to fully opaque
+        block[0] = 0xFF; // A0
+        block[1] = 0xFF; // A1
+
+        // Set some alpha indices
+        for x in 2..8 {
+            block[x] = 0; // All indices point to A0
+        }
+
+        // Set some color values (doesn't matter for this test)
+        for x in 8..16 {
+            block[x] = x as u8;
+        }
+
+        // Create output buffer
+        let mut output = [0u8; 16];
+
+        // Normalize the block
+        unsafe {
+            normalize_blocks(
+                block.as_ptr(),
+                output.as_mut_ptr(),
+                16,
+                AlphaNormalizationMode::OpaqueZeroAlphaMaxIndices,
+                ColorNormalizationMode::None,
+            );
+        }
+
+        // Verify alpha normalization
+        // First two bytes should be zero
+        assert_eq!(output[0], 0);
+        assert_eq!(output[1], 0);
+
+        // All index bytes should be 0xFF
+        for x in 2..8 {
+            assert_eq!(output[x], 0xFF);
+        }
+
+        // Color bytes should be unchanged
+        for x in 8..16 {
+            assert_eq!(output[x], block[x]);
+        }
+
+        // Decode the normalized block to verify alphas are still 255
+        let decoded = unsafe { decode_bc3_block(output.as_ptr()) };
+        for x in 0..16 {
+            assert_eq!(decoded.pixels[x].a, 255);
+        }
+    }
+
+    /// Test that non-opaque uniform alpha is properly normalized
+    #[test]
+    fn can_normalize_non_opaque_uniform_alpha() {
+        // Create a BC3 block with semi-transparent alpha
+        let mut block = [0u8; 16];
+
+        // Set alpha endpoints to 128 (semi-transparent)
+        block[0] = 128; // A0
+        block[1] = 128; // A1
+
+        // Set all alpha indices to point to A0
+        for x in 2..8 {
+            block[x] = 0;
+        }
+
+        // Set some color values (doesn't matter for this test)
+        for x in 8..16 {
+            block[x] = x as u8;
+        }
+
+        // Create output buffer
+        let mut output = [0u8; 16];
+
+        // Test all alpha modes
+        let modes = [
+            AlphaNormalizationMode::UniformAlphaZeroIndices,
+            AlphaNormalizationMode::OpaqueFillAll,
+            AlphaNormalizationMode::OpaqueZeroAlphaMaxIndices,
+        ];
+
+        for mode in modes {
+            // Normalize the block
+            unsafe {
+                normalize_blocks(
+                    block.as_ptr(),
+                    output.as_mut_ptr(),
+                    16,
+                    mode,
+                    ColorNormalizationMode::None,
+                );
+            }
+
+            // For non-opaque alpha, all modes should behave like UniformAlphaZeroIndices
+            assert_eq!(output[0], 128); // A0 = alpha value
+            assert_eq!(output[1], 0); // A1 = 0
+
+            // All index bytes should be 0
+            for x in 2..8 {
+                assert_eq!(output[x], 0);
+            }
+
+            // Decode the normalized block to verify alphas are still 128
+            let decoded = unsafe { decode_bc3_block(output.as_ptr()) };
+            for x in 0..16 {
+                assert_eq!(decoded.pixels[x].a, 128);
+            }
+        }
+    }
+
+    /// Test normalizing multiple blocks in one call
+    #[test]
+    fn can_normalize_multiple_blocks() {
+        // Create two BC3 blocks
+        let mut blocks = [0u8; 32]; // 2 blocks, 16 bytes each
+
+        // Block 1: Solid red with uniform alpha
+        // Set alpha endpoints to fully opaque
+        blocks[0] = 0xFF;
+        blocks[1] = 0xFF;
+
+        // Set alpha indices
+        for x in 2..8 {
+            blocks[x] = 0;
+        }
+
+        // Set Color0 to red, Color1 to something else
+        let red565 = 0xF800u16.to_le_bytes();
+        blocks[8] = red565[0];
+        blocks[9] = red565[1];
+        blocks[10] = 0x12;
+        blocks[11] = 0x34;
+
+        // Set indices to all point to Color0
+        for x in 12..16 {
+            blocks[x] = 0;
+        }
+
+        // Block 2: Explicitly set to have mixed alpha and colors
+        // This should not get normalized
+
+        // Set different alpha endpoints
+        blocks[16] = 0x80; // A0
+        blocks[17] = 0x40; // A1
+
+        // Important: Need different alpha indices to create truly mixed alpha values
+        blocks[18] = 0xAA; // Mixed pattern of indices
+        blocks[19] = 0x55; // Different pattern
+        blocks[20] = 0x33; // Different pattern
+        blocks[21] = 0xCC; // Different pattern
+        blocks[22] = 0x0F; // Different pattern
+        blocks[23] = 0xF0; // Different pattern
+
+        // Set different colors in Color0 and Color1
+        let green565 = 0x07E0u16.to_le_bytes();
+        blocks[24] = green565[0]; // Green in Color0
+        blocks[25] = green565[1];
+
+        let blue565 = 0x001Fu16.to_le_bytes();
+        blocks[26] = blue565[0]; // Blue in Color1
+        blocks[27] = blue565[1];
+
+        // Set truly mixed color indices
+        blocks[28] = 0x55; // Mixed pattern 01 01 01 01
+        blocks[29] = 0xAA; // Mixed pattern 10 10 10 10
+        blocks[30] = 0x3C; // Mixed pattern 00 11 11 00
+        blocks[31] = 0x69; // Mixed pattern 01 10 01 00
+
+        // Create a copy of the blocks for later comparison
+        let blocks_copy = blocks;
+
+        // Create output buffer
+        let mut output = [0u8; 32];
+
+        // Normalize the blocks
+        unsafe {
+            normalize_blocks(
+                blocks.as_ptr(),
+                output.as_mut_ptr(),
+                32,
+                AlphaNormalizationMode::UniformAlphaZeroIndices,
+                ColorNormalizationMode::Color0Only,
+            );
+        }
+
+        // Block 1 should be normalized for both alpha and color
+        assert_eq!(output[0], 0xFF); // A0 = alpha value
+        assert_eq!(output[1], 0); // A1 = 0
+
+        // Alpha indices should be 0 (UniformAlphaZeroIndices)
+        for x in 2..8 {
+            assert_eq!(output[x], 0);
+        }
+
+        // Color0 should be red, Color1 and indices should be 0 (Color0Only mode)
+        assert_eq!(output[8], red565[0]);
+        assert_eq!(output[9], red565[1]);
+        assert_eq!(output[10], 0);
+        assert_eq!(output[11], 0);
+
+        for x in 12..16 {
+            assert_eq!(output[x], 0);
+        }
+
+        // Block 2 should be completely unchanged as it has mixed colors and mixed alpha
+        for x in 16..32 {
+            assert_eq!(
+                output[x], blocks_copy[x],
+                "Block 2 byte {x} was modified when it should be unchanged",
+            );
+        }
+    }
+}
diff --git a/projects/dxt-lossless-transform-bc3/src/util/bc3_decode.rs b/projects/dxt-lossless-transform-bc3/src/util/bc3_decode.rs
index c72f7e7b..3307284a 100644
--- a/projects/dxt-lossless-transform-bc3/src/util/bc3_decode.rs
+++ b/projects/dxt-lossless-transform-bc3/src/util/bc3_decode.rs
@@ -96,8 +96,8 @@ pub unsafe fn decode_bc3_block(src: *const u8) -> Decoded4x4Block {
 
     // Create alpha lookup table
     let mut alpha_values = [0u8; 8];
-    alpha_values[0] = alpha0;
-    alpha_values[1] = alpha1;
+    alpha_values[0] = alpha0; // bit code 000
+    alpha_values[1] = alpha1; // bit code 001
 
     // In BC4/BC3, if alpha0 > alpha1, we have 8 interpolated values
     // Otherwise we have 6 interpolated values plus transparent and opaque
diff --git a/projects/dxt-lossless-transform-common/src/decoded_4x4_block.rs b/projects/dxt-lossless-transform-common/src/decoded_4x4_block.rs
index 556eff73..3cf6a8c5 100644
--- a/projects/dxt-lossless-transform-common/src/decoded_4x4_block.rs
+++ b/projects/dxt-lossless-transform-common/src/decoded_4x4_block.rs
@@ -74,9 +74,6 @@ impl Decoded4x4Block {
     /// `true` if all pixels in the block are identical, `false` otherwise
     #[inline]
     pub fn has_identical_pixels_ignore_alpha(&self) -> bool {
-        // Assert at compile time that Color8888 is the same size as u32
-        const _: () = assert!(size_of::<Color8888>() == size_of::<u32>());
-
         // Get first pixel without alpha
         let first_pixel_u32_no_alpha = self.pixels[0].without_alpha();
 
@@ -85,4 +82,11 @@ impl Decoded4x4Block {
             .iter()
             .all(|pixel| pixel.without_alpha() == first_pixel_u32_no_alpha)
     }
+
+    /// Checks if all pixels in the block have the same alpha values
+    #[inline]
+    pub fn has_identical_alpha(&self) -> bool {
+        let first_pixel_alpha = self.pixels[0].a;
+        self.pixels.iter().all(|pixel| pixel.a == first_pixel_alpha)
+    }
 }


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>fuzz/fuzz_targets/bc2_normalize.rs (1)</summary><blockquote>

`7-25`: **Good addition of GPU compatibility check.**

The added check helps avoid testing blocks where c0 ≤ c1, which are not supported on all GPUs. This ensures that fuzzing focuses on valid, widely-compatible configurations.


Consider adding a brief comment explaining why this specific condition (c0 > c1) is required for GPU compatibility. This would help future maintainers understand the technical constraint.

```diff
-    // Skip if c0 <= c1 as that mode is not supported on all GPUs.
+    // Skip if c0 <= c1 as this mode is not supported on all GPUs.
+    // BC2 format specification requires c0 > c1 for proper 4-color mode decoding.
```

</blockquote></details>
<details>
<summary>projects/dxt-lossless-transform-bc3/README.MD (1)</summary><blockquote>

`56-106`: **Great documentation on BC3 block normalization approach!**

The explanation of the normalization process is detailed and clear. It effectively covers both the alpha normalization modes and the color normalization approach.


There appears to be a small typo in the formatting around line 81, where `0xFF` might be repeated unnecessarily in the text.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[duplication] ~81-~81: Possible typo: you repeated a word.
Context: ... be used:  - All 8 bytes set to `0xFF` (`0xFFFFFFFF 0xFFFFFFFF`).   - Because `A0` <= `A1`, index 0xFF...

(ENGLISH_WORD_REPEAT_RULE)

</details>

</details>

</blockquote></details>
<details>
<summary>fuzz/fuzz_targets/bc3_normalize.rs (1)</summary><blockquote>

`30-78`: **Comprehensive testing of all normalization mode combinations.**

The fuzz test does an excellent job of testing all combinations of alpha and color normalization modes while providing detailed error messages in case of failures. The unsafe blocks are justified for low-level graphics code dealing with raw memory.


Consider adding a brief comment explaining why the call to `decode_bc3_block` needs to be unsafe. This would make the safety requirements clearer for readers.

</blockquote></details>
<details>
<summary>projects/dxt-lossless-transform-bc3/benches/normalize_blocks/main.rs (1)</summary><blockquote>

`98-109`: **Use `black_box` to avoid dead-code elimination and make the benchmark stable**

The benchmark never observes `output_ptr`, so LLVM might legally
eliminate the entire `normalize_blocks` call if it can prove there are
no externally-visible effects.  Wrapping the pointers with
`criterion::black_box` guarantees the work is retained.

```diff
 b.iter(|| unsafe {
-    normalize_blocks(
-        input_ptr,
-        output_ptr,
+    normalize_blocks(
+        criterion::black_box(input_ptr),
+        criterion::black_box(output_ptr),
         file_size,
         AlphaNormalizationMode::UniformAlphaZeroIndices,
         ColorNormalizationMode::Color0Only,
     );
 })
```

</blockquote></details>
<details>
<summary>projects/dxt-lossless-transform-bc3/src/normalize_blocks/mod.rs (2)</summary><blockquote>

`111-122`: **Avoid a full decode when no normalisation is requested**

`decode_bc3_block` is relatively heavy and is called unconditionally for
every block, even when `has_uniform_alpha == false &&
has_solid_color == false`.  A cheap pre-filter can skip most blocks
(e.g. early test the eight alpha bytes for uniformity and the four
colour bytes for equality) and only fall back to the full decode when
one of those quick checks passes.  This should translate to a noticeable
speed-up on real textures where only a small fraction of blocks are
single-colour / uniform-alpha.

---

`255-268`: **Eliminate the self-recursive call for non-opaque paths**

`OpaqueFillAll` falls back to `UniformAlphaZeroIndices` via a recursive
call to `normalize_alpha`.  This is small but unnecessary extra work and
makes the control-flow harder to read.  A simple `match` or explicit
branch copy achieves the same effect without recursion.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between b776bc8c2b5d633c1a14e973bb979a6820931b02 and 1f74f2a452bba6fe3d12197c9ebffdf605a5dfd9.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `fuzz/Cargo.toml` (1 hunks)
* `fuzz/fuzz_targets/bc2_normalize.rs` (2 hunks)
* `fuzz/fuzz_targets/bc3_normalize.rs` (1 hunks)
* `projects/dxt-lossless-transform-bc3/Cargo.toml` (2 hunks)
* `projects/dxt-lossless-transform-bc3/README.MD` (1 hunks)
* `projects/dxt-lossless-transform-bc3/benches/normalize_blocks/main.rs` (1 hunks)
* `projects/dxt-lossless-transform-bc3/src/lib.rs` (1 hunks)
* `projects/dxt-lossless-transform-bc3/src/normalize_blocks/mod.rs` (1 hunks)
* `projects/dxt-lossless-transform-bc3/src/util/bc3_decode.rs` (1 hunks)
* `projects/dxt-lossless-transform-common/src/decoded_4x4_block.rs` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>fuzz/fuzz_targets/bc2_normalize.rs (1)</summary><blockquote>

<details>
<summary>projects/dxt-lossless-transform-common/src/color_565.rs (2)</summary>

* `from_raw` (15-17)
* `greater_than` (84-86)

</details>

</blockquote></details>
<details>
<summary>projects/dxt-lossless-transform-bc3/src/lib.rs (1)</summary><blockquote>

<details>
<summary>projects/dxt-lossless-transform-bc3/src/normalize_blocks/mod.rs (1)</summary>

* `normalize_blocks` (91-168)

</details>

</blockquote></details>
<details>
<summary>projects/dxt-lossless-transform-bc3/src/normalize_blocks/mod.rs (2)</summary><blockquote>

<details>
<summary>projects/dxt-lossless-transform-bc3/src/util/bc3_decode.rs (1)</summary>

* `decode_bc3_block` (43-173)

</details>
<details>
<summary>projects/dxt-lossless-transform-common/src/decoded_4x4_block.rs (1)</summary>

* `new` (25-29)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>projects/dxt-lossless-transform-bc3/README.MD</summary>

[duplication] ~81-~81: Possible typo: you repeated a word.
Context: ... be used:  - All 8 bytes set to `0xFF` (`0xFFFFFFFF 0xFFFFFFFF`).   - Because `A0` <= `A1`, index 0xFF...

(ENGLISH_WORD_REPEAT_RULE)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: test (ubuntu-latest, i686-unknown-linux-gnu, false, nightly, nightly)
* GitHub Check: test (ubuntu-latest, aarch64-unknown-linux-gnu, true, stable)
* GitHub Check: test (windows-latest, i686-pc-windows-msvc, false, stable)
* GitHub Check: test (windows-latest, x86_64-pc-windows-msvc, false, stable)
* GitHub Check: build-binaries (macos-13, x86_64-apple-darwin, false)
* GitHub Check: build-binaries (ubuntu-latest, powerpc64-unknown-linux-gnu, true)
* GitHub Check: build-binaries (windows-latest, i686-pc-windows-msvc, false)
* GitHub Check: build-binaries (windows-latest, x86_64-pc-windows-msvc, false)

</details>

<details>
<summary>🔇 Additional comments (10)</summary><blockquote>

<details>
<summary>projects/dxt-lossless-transform-bc3/src/lib.rs (1)</summary>

`6-6`: **LGTM: Good addition of the normalization module.**

Adding this public module is a clean way to expose the new BC3 block normalization functionality.

</details>
<details>
<summary>projects/dxt-lossless-transform-bc3/src/util/bc3_decode.rs (1)</summary>

`99-100`: **Documentation improvement - nice!**

The added comments clearly identify the bit codes associated with each alpha value, which improves code readability.

</details>
<details>
<summary>projects/dxt-lossless-transform-bc3/Cargo.toml (2)</summary>

`24-24`: **LGTM: New dependency for performance optimization.**

The `likely_stable` crate provides branch prediction hints which can improve performance in the critical path of texture processing.

---

`50-53`: **LGTM: New benchmark for normalize_blocks functionality.**

Adding a benchmark for the new normalization features is a good practice that will help track performance over time.

</details>
<details>
<summary>projects/dxt-lossless-transform-common/src/decoded_4x4_block.rs (1)</summary>

`85-91`: **LGTM! The `has_identical_alpha` method is a great addition.**

The implementation is clean, efficient, and follows the pattern of existing methods in this struct. This functionality will be useful for optimizing BC3 block normalization cases where all pixels share the same alpha value.

</details>
<details>
<summary>fuzz/Cargo.toml (1)</summary>

`59-66`: **LGTM! Proper configuration for the new BC3 fuzz target.**

The BC3 normalize target is set up consistently with other fuzz targets in the project.

</details>
<details>
<summary>fuzz/fuzz_targets/bc3_normalize.rs (2)</summary>

`1-20`: **Well-structured fuzz target setup with comprehensive comments.**

The file header, imports, and Bc3Block structure are well-organized. Good use of the arbitrary trait for fuzzing input generation.

---

`21-29`: **Good safety check for GPU compatibility.**

Filtering out blocks where c0 <= c1 is important to prevent testing unsupported GPU modes, consistent with the approach described in the README.

</details>
<details>
<summary>projects/dxt-lossless-transform-bc3/benches/normalize_blocks/main.rs (1)</summary>

`17-20`: _🛠️ Refactor suggestion_

**Handle zero-byte allocations and fallible `Layout` creation**

`Layout::from_size_align(num_bytes, 64).unwrap()` will panic for
`num_bytes == 0`, and even a zero-length slice may legitimately appear
(e.g. an empty test file).  In addition, `RawAlloc::new` can fail but is
unconditionally unwrapped, aborting the whole benchmark.

```diff
-pub(crate) fn allocate_align_64(num_bytes: usize) -> RawAlloc {
-    let layout = Layout::from_size_align(num_bytes, 64).unwrap();
-    RawAlloc::new(layout).unwrap()
+pub(crate) fn allocate_align_64(num_bytes: usize) -> RawAlloc {
+    assert!(num_bytes != 0, "attempted to allocate 0 bytes");
+    let layout =
+        Layout::from_size_align(num_bytes, 64).expect("invalid layout for 64-byte alignment");
+    RawAlloc::new(layout).expect("memory allocation for benchmark failed")
 }
```




> Likely an incorrect or invalid review comment.

</details>
<details>
<summary>projects/dxt-lossless-transform-bc3/src/normalize_blocks/mod.rs (1)</summary>

`312-350`: **Endian-safety for `u16::to_le_bytes`**

`color565.to_le_bytes()` always produces little-endian order, but BC3
blocks are specified as little-endian only on little-endian
architectures.  When compiled for big-endian targets this will write the
wrong byte order.  Consider using `to_le_bytes` only when
`cfg!(target_endian = "little")` or explicitly encoding in little-endian
form regardless of host endianness via `u16::to_le_bytes`,
`u16::to_be_bytes` based on the BC3 spec expectations.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-11T22:29:16Z

Review comments:
diff_hunk: @@ -0,0 +1,128 @@
+use core::{alloc::Layout, ptr::copy_nonoverlapping};
+use criterion::{criterion_group, criterion_main, Criterion};
+use dxt_lossless_transform_bc3::normalize_blocks::{
+    normalize_blocks, AlphaNormalizationMode, ColorNormalizationMode,
+};
+use safe_allocator_api::RawAlloc;
+use std::fs;
+
+#[cfg(not(target_os = "windows"))]
+use pprof::criterion::{Output, PProfProfiler};
+
+// Path to the BC3 file to benchmark with
+// You may need to update this path to point to a valid BC3 texture file on your system
+const TEST_FILE_PATH: &str =
+    "/home/sewer/Temp/texture-stuff/bc3-raw/202x-architecture-10.01/whiterun/wrcastlecarpets01.dds";
+
+pub(crate) fn allocate_align_64(num_bytes: usize) -> RawAlloc {
+    let layout = Layout::from_size_align(num_bytes, 64).unwrap();
+    RawAlloc::new(layout).unwrap()
+}
+
+#[allow(clippy::needless_range_loop)]
+fn criterion_benchmark(c: &mut Criterion) {
+    let mut group = c.benchmark_group("BC3 Normalize Blocks");
+
+    // Try to load the test file
+    let file_data = match fs::read(TEST_FILE_PATH) {
+        Ok(data) => {
+            println!("Successfully loaded test file: {TEST_FILE_PATH}");
+            data
+        }
+        Err(e) => {
+            println!("Warning: Could not load test file '{TEST_FILE_PATH}': {e}");
+            println!("To run this benchmark, you need a valid BC3 texture file.");
+            println!("Please update the TEST_FILE_PATH constant to point to a valid BC3 file or create a sample BC3 file.");
+            println!("Alternatively, we'll create a synthetic BC3 file for benchmarking.");
+
+            // Create a synthetic file with 1024 identical BC3 blocks for benchmarking
+            // BC3 block is 16 bytes (8 for alpha, 4 for colors, 4 for indices)
+            let block_size = 16;
+            let num_blocks = 1024;
+            let total_size = block_size * num_blocks;
+
+            // Create a sample BC3 block with solid red color
+            let mut sample_block = [0u8; 16];
+
+            // Alpha part (first 8 bytes) - all fully opaque (255)
+            for x in 0..8 {
+                sample_block[x] = 0xFF;
+            }
+
+            // Color endpoints (RGB565)
+            // Red = 0xF800 in RGB565 (little endian: [0x00, 0xF8])
+            sample_block[8] = 0x00;
+            sample_block[9] = 0xF8;
+            sample_block[10] = 0x00; // Second color not used
+            sample_block[11] = 0x00;
+
+            // Indices (all 0)
+            sample_block[12] = 0;
+            sample_block[13] = 0;
+            sample_block[14] = 0;
+            sample_block[15] = 0;
+
+            // Create the synthetic file by repeating the sample block
+            let mut synthetic_data = Vec::with_capacity(total_size);
+            for _ in 0..num_blocks {
+                synthetic_data.extend_from_slice(&sample_block);
+            }
+
+            println!(
+                "Created synthetic BC3 file with {} blocks ({} bytes) for benchmarking.",
+                num_blocks,
+                synthetic_data.len()
+            );
+            synthetic_data
+        }
+    };
+
+    // Ensure we have a file size that's a multiple of 16 (BC3 block size)
+    let file_size = file_data.len();
+
+    // Allocate memory for input and output
+    let mut input = allocate_align_64(file_size);
+    let mut output = allocate_align_64(file_size);
+
path: projects/dxt-lossless-transform-bc3/benches/normalize_blocks/main.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Prevent UB by validating block count at run-time**

`normalize_blocks` has only a `debug_assert!(len % 16 == 0)`.  
In release benchmarks that assertion is stripped and passing a file
whose size is not a multiple of 16 bytes causes out-of-bounds reads/
writes inside the unsafe normaliser.

```diff
 // Ensure we have a file size that's a multiple of 16 (BC3 block size)
 let file_size = file_data.len();
+
+if file_size % 16 != 0 {
+    panic!(
+        "Input size ({}) is not divisible by 16; cannot form complete BC3 blocks",
+        file_size
+    );
+}
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-11T22:29:15Z
author_association: NONE
diff_hunk: @@ -0,0 +1,877 @@
+//! # Block Normalization Process
+//!
+//! This module contains the code used to normalize BC3 blocks to improve compression ratio
+//! by making solid color blocks and alpha values have consistent representations.
+//!
+//! ## BC3 Block Format
+//!
+//! First, let's recall the BC3 block format:
+//!
+//! ```text
+//! Address: 0       2        8       12      16
+//!          +-------+--------+  +-------+--------+
+//! Data:    | A0-A1 |AI0-AI15|  | C0-C1 | I0-I15 |
+//!          +-------+--------+  +-------+--------+
+//! ```
+//!
+//! Where:
+//! - `A0-A1` are two alpha endpoints (8-bit each)
+//! - `AI0-AI15` are 6 bytes containing sixteen 3-bit alpha indices
+//! - `C0-C1` are 16-bit RGB565 color values (2 bytes each)
+//! - `I0-I15` are 4 bytes containing sixteen 2-bit color indices
+//!
+//! ## Normalization Rules
+//!
+//! The normalization process applies different rules for alpha and color components:
+//!
+//! ### Alpha Normalization
+//!
+//! When an entire block has uniform alpha, several representations are possible:
+//!
+//! For fully opaque blocks (`0xFF`):
+//! - All 8 bytes set to `0xFF` i.e. (`0xFFFFFFFF 0xFFFFFFFF`).
+//!   - Because `A0` <= `A1`, index `0xFF` is hardcoded to opaque on the decoder side.
+//! - Zero alphas but indices set to `0xFF` i.e. (`0x0000FFFF 0xFFFFFFFF`).
+//!
+//! For all other values (including `0xFF`):
+//! - `A0` set to the alpha value, everything else to `0x00` i.e. (`0xFF000000 0x00000000`).
+//!   - Everything uses the alpha value from the first endpoint.
+//!
+//! ### Color Normalization
+//!
+//! For solid color blocks with clean RGB565 conversion:
+//! - Set color in `C0`, zeroes in `C1` and indices
+//!   - This results in a nice repetition of `0x00` across 6 bytes
+//! - Or replicate color in both `C0` and `C1`, zeroes in indices
+//!   - In some cases, this performs better in compression
+//!
+//! Note: With BC3, it's important that we put the color in `C0` because the 'alternate alpha mode' of
+//! BC1 where `c0 <= c1` is unsupported; it leads to undefined behavior on some GPUs.
+//!
+//! ## Implementation Details
+//!
+//! The normalization process:
+//!
+//! 1. Decodes the block to get all 16 pixels with their colors and alpha values
+//! 2. Checks for uniform alpha and solid colors
+//! 3. Applies appropriate normalization based on the selected modes
+//! 4. Writes the normalized block to the output
+
+use crate::util::decode_bc3_block;
+use core::ptr::copy_nonoverlapping;
+use dxt_lossless_transform_common::color_8888::Color8888;
+use likely_stable::unlikely;
+
+/// Reads an input of BC3 blocks from `input_ptr` and writes the normalized blocks to `output_ptr`.
+///
+/// # Parameters
+///
+/// - `input_ptr`: A pointer to the input data (input BC3 blocks)
+/// - `output_ptr`: A pointer to the output data (output BC3 blocks)
+/// - `len`: The length of the input data in bytes
+/// - `alpha_mode`: How to normalize alpha values
+/// - `color_mode`: How to normalize color values
+///
+/// # Safety
+///
+/// - input_ptr must be valid for reads of len bytes
+/// - output_ptr must be valid for writes of len bytes
+/// - len must be divisible by 16 (BC3 block size)
+///
+/// # Remarks
+///
+/// This function identifies and normalizes BC3 blocks based on their content:
+/// - Blocks with uniform alpha are normalized according to the alpha_mode
+/// - Solid color blocks are normalized according to the color_mode
+/// - Other blocks are preserved as-is
+///
+/// Normalization improves compression ratios by ensuring that similar visual blocks
+/// have identical binary representations, reducing entropy in the data.
+#[inline]
+pub unsafe fn normalize_blocks(
+    input_ptr: *const u8,
+    output_ptr: *mut u8,
+    len: usize,
+    alpha_mode: AlphaNormalizationMode,
+    color_mode: ColorNormalizationMode,
+) {
+    debug_assert!(len % 16 == 0);
+
+    // Skip normalization if both modes are None
+    if alpha_mode == AlphaNormalizationMode::None && color_mode == ColorNormalizationMode::None {
+        copy_nonoverlapping(input_ptr, output_ptr, len);
+        return;
+    }
path: projects/dxt-lossless-transform-bc3/src/normalize_blocks/mod.rs
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Replace `debug_assert` with a hard check to keep release builds safe**

If `len` is not divisible by 16 in a release build, the pointer math at
the bottom of the loop will silently walk past the end of the buffer and
corrupt memory.  A fast unconditional assert is still cheaper than
undefined behaviour.

```diff
-debug_assert!(len % 16 == 0);
+assert!(
+    len % 16 == 0,
+    "length ({len}) is not a multiple of 16 bytes — cannot process partial BC3 blocks"
+);
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
     assert!(
         len % 16 == 0,
         "length ({len}) is not a multiple of 16 bytes — cannot process partial BC3 blocks"
     );

     // Skip normalization if both modes are None
     if alpha_mode == AlphaNormalizationMode::None && color_mode == ColorNormalizationMode::None {
         copy_nonoverlapping(input_ptr, output_ptr, len);
         return;
     }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-11T22:29:15Z
author_association: NONE
diff_hunk: @@ -0,0 +1,877 @@
+//! # Block Normalization Process
+//!
+//! This module contains the code used to normalize BC3 blocks to improve compression ratio
+//! by making solid color blocks and alpha values have consistent representations.
+//!
+//! ## BC3 Block Format
+//!
+//! First, let's recall the BC3 block format:
+//!
+//! ```text
+//! Address: 0       2        8       12      16
+//!          +-------+--------+  +-------+--------+
+//! Data:    | A0-A1 |AI0-AI15|  | C0-C1 | I0-I15 |
+//!          +-------+--------+  +-------+--------+
+//! ```
+//!
+//! Where:
+//! - `A0-A1` are two alpha endpoints (8-bit each)
+//! - `AI0-AI15` are 6 bytes containing sixteen 3-bit alpha indices
+//! - `C0-C1` are 16-bit RGB565 color values (2 bytes each)
+//! - `I0-I15` are 4 bytes containing sixteen 2-bit color indices
+//!
+//! ## Normalization Rules
+//!
+//! The normalization process applies different rules for alpha and color components:
+//!
+//! ### Alpha Normalization
+//!
+//! When an entire block has uniform alpha, several representations are possible:
+//!
+//! For fully opaque blocks (`0xFF`):
+//! - All 8 bytes set to `0xFF` i.e. (`0xFFFFFFFF 0xFFFFFFFF`).
+//!   - Because `A0` <= `A1`, index `0xFF` is hardcoded to opaque on the decoder side.
+//! - Zero alphas but indices set to `0xFF` i.e. (`0x0000FFFF 0xFFFFFFFF`).
+//!
+//! For all other values (including `0xFF`):
+//! - `A0` set to the alpha value, everything else to `0x00` i.e. (`0xFF000000 0x00000000`).
+//!   - Everything uses the alpha value from the first endpoint.
+//!
+//! ### Color Normalization
+//!
+//! For solid color blocks with clean RGB565 conversion:
+//! - Set color in `C0`, zeroes in `C1` and indices
+//!   - This results in a nice repetition of `0x00` across 6 bytes
+//! - Or replicate color in both `C0` and `C1`, zeroes in indices
+//!   - In some cases, this performs better in compression
+//!
+//! Note: With BC3, it's important that we put the color in `C0` because the 'alternate alpha mode' of
+//! BC1 where `c0 <= c1` is unsupported; it leads to undefined behavior on some GPUs.
+//!
+//! ## Implementation Details
+//!
+//! The normalization process:
+//!
+//! 1. Decodes the block to get all 16 pixels with their colors and alpha values
+//! 2. Checks for uniform alpha and solid colors
+//! 3. Applies appropriate normalization based on the selected modes
+//! 4. Writes the normalized block to the output
+
+use crate::util::decode_bc3_block;
+use core::ptr::copy_nonoverlapping;
+use dxt_lossless_transform_common::color_8888::Color8888;
+use likely_stable::unlikely;
+
+/// Reads an input of BC3 blocks from `input_ptr` and writes the normalized blocks to `output_ptr`.
+///
+/// # Parameters
+///
+/// - `input_ptr`: A pointer to the input data (input BC3 blocks)
+/// - `output_ptr`: A pointer to the output data (output BC3 blocks)
+/// - `len`: The length of the input data in bytes
+/// - `alpha_mode`: How to normalize alpha values
+/// - `color_mode`: How to normalize color values
+///
+/// # Safety
+///
+/// - input_ptr must be valid for reads of len bytes
+/// - output_ptr must be valid for writes of len bytes
+/// - len must be divisible by 16 (BC3 block size)
+///
path: projects/dxt-lossless-transform-bc3/src/normalize_blocks/mod.rs
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Document and enforce non-overlapping buffers**

`copy_nonoverlapping` is used several times, yet the public safety
contract does not forbid `input_ptr == output_ptr`. Passing aliased
buffers would immediately cause UB.  Please state this requirement in
the doc-comment and guard against it at run-time:

```diff
 /// - `output_ptr` must be valid for writes of `len` bytes
+/// - The regions referenced by `input_ptr` and `output_ptr` must **not overlap**
+
+debug_assert!(
+    input_ptr.add(len) <= output_ptr || output_ptr.add(len) <= input_ptr,
+    "input and output ranges must not overlap"
+);
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
/// Reads an input of BC3 blocks from `input_ptr` and writes the normalized blocks to `output_ptr`.
///
/// # Parameters
///
/// - `input_ptr`: A pointer to the input data (input BC3 blocks)
/// - `output_ptr`: A pointer to the output data (output BC3 blocks)
/// - `len`: The length of the input data in bytes
/// - `alpha_mode`: How to normalize alpha values
/// - `color_mode`: How to normalize color values
///
/// # Safety
///
/// - `input_ptr` must be valid for reads of `len` bytes
/// - `output_ptr` must be valid for writes of `len` bytes
/// - The regions referenced by `input_ptr` and `output_ptr` must **not overlap**
/// - `len` must be divisible by 16 (BC3 block size)
///
pub unsafe fn normalize_blocks(
    input_ptr: *const u8,
    output_ptr: *mut u8,
    len: usize,
    alpha_mode: AlphaMode,
    color_mode: ColorMode,
) {
    debug_assert!(
        input_ptr.add(len) <= output_ptr || output_ptr.add(len) <= input_ptr,
        "input and output ranges must not overlap"
    );

    // … rest of implementation …
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-11T22:29:15Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-05-11T22:25:34Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

A comprehensive normalization feature for BC3 texture blocks was introduced, including a new Rust module with normalization logic, public APIs, benchmarking, fuzz testing, and expanded documentation. The changes enable configurable normalization of alpha and color data in BC3 blocks, with tests and benchmarks to ensure correctness and performance. Supporting utilities and documentation were updated accordingly.

## Changes

| File(s)                                                                                   | Change Summary |
|-------------------------------------------------------------------------------------------|---------------|
| `projects/dxt-lossless-transform-bc3/src/normalize_blocks/mod.rs`                         | New module implementing BC3 block normalization with configurable alpha and color modes, public API, and tests. |
| `projects/dxt-lossless-transform-bc3/src/lib.rs`                                          | Declared new public `normalize_blocks` module. |
| `projects/dxt-lossless-transform-bc3/benches/normalize_blocks/main.rs`                    | Added Criterion benchmark for `normalize_blocks`, supporting file-based and synthetic BC3 data, with profiling. |
| `projects/dxt-lossless-transform-bc3/Cargo.toml`                                          | Added `likely_stable` dependency and registered new benchmark target for normalization. |
| `projects/dxt-lossless-transform-bc3/README.MD`                                           | Expanded and clarified documentation on BC3 block normalization, especially for single-color/alpha blocks. |
| `projects/dxt-lossless-transform-bc3/src/util/bc3_decode.rs`                              | Added inline comments clarifying alpha lookup table bit codes in BC3 decoding logic. |
| `projects/dxt-lossless-transform-common/src/decoded_4x4_block.rs`                         | Added method to check for uniform alpha in a decoded block; removed a compile-time assertion. |
| `fuzz/Cargo.toml`                                                                         | Registered new fuzz target for BC3 normalization and cleaned up previous BC2 target configuration. |
| `fuzz/fuzz_targets/bc2_normalize.rs`                                                      | Enhanced BC2 normalization fuzz test with a precondition for valid color endpoint ordering. |
| `fuzz/fuzz_targets/bc3_normalize.rs`                                                      | Introduced new fuzz test for BC3 normalization, validating pixel equivalence across normalization modes. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Caller
    participant normalize_blocks
    participant DecodedBlock
    participant OutputBuffer

    Caller->>normalize_blocks: Call with input_ptr, output_ptr, len, alpha_mode, color_mode
    loop For each BC3 block
        normalize_blocks->>DecodedBlock: Decode input block to pixels/alpha
        normalize_blocks->>normalize_blocks: Check for uniform alpha/color
        alt Normalization applicable
            normalize_blocks->>OutputBuffer: Write normalized block (alpha/color modes)
        else Not applicable
            normalize_blocks->>OutputBuffer: Copy original block
        end
    end
    normalize_blocks-->>Caller: Return
```

## Poem

> In byte-sized fields where textures play,  
> The blocks align in a normalized way.  
> Alpha and color, now tidy and neat,  
> Benchmarks and fuzzers ensure they compete.  
> With docs expanded and tests anew,  
> This bunny hops with pride—BC3, we’ve tamed you!  
> 🐇✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Sewer56/dxt-lossless-transform&utm_content=29):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-11T22:26:21Z
body: ## [Codecov](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/pull/29?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56) Report
Attention: Patch coverage is `86.20690%` with `12 lines` in your changes missing coverage. Please review.
> Project coverage is 72.67%. Comparing base [(`b776bc8`)](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/commit/b776bc8c2b5d633c1a14e973bb979a6820931b02?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56) to head [(`1f74f2a`)](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/commit/1f74f2a452bba6fe3d12197c9ebffdf605a5dfd9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56).
> Report is 5 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/pull/29?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56) | Patch % | Lines |
|---|---|---|
| [...lossless-transform-bc3/src/normalize\_blocks/mod.rs](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/pull/29?src=pr&el=tree&filepath=projects%2Fdxt-lossless-transform-bc3%2Fsrc%2Fnormalize_blocks%2Fmod.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56#diff-cHJvamVjdHMvZHh0LWxvc3NsZXNzLXRyYW5zZm9ybS1iYzMvc3JjL25vcm1hbGl6ZV9ibG9ja3MvbW9kLnJz) | 85.36% | [12 Missing :warning: ](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/pull/29?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56) |

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/pull/29/graphs/tree.svg?width=650&height=150&src=pr&token=mSqTg8vzAQ&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56)](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/pull/29?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56)

```diff
@@            Coverage Diff             @@
##             main      #29      +/-   ##
==========================================
+ Coverage   72.43%   72.67%   +0.23%     
==========================================
  Files          57       58       +1     
  Lines        4760     4845      +85     
==========================================
+ Hits         3448     3521      +73     
- Misses       1312     1324      +12     
```

| [Flag](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/pull/29/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/pull/29/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56) | `72.67% <86.20%> (+0.23%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files with missing lines](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/pull/29?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56) | Coverage Δ | |
|---|---|---|
| [projects/dxt-lossless-transform-bc3/src/lib.rs](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/pull/29?src=pr&el=tree&filepath=projects%2Fdxt-lossless-transform-bc3%2Fsrc%2Flib.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56#diff-cHJvamVjdHMvZHh0LWxvc3NsZXNzLXRyYW5zZm9ybS1iYzMvc3JjL2xpYi5ycw==) | `0.00% <ø> (ø)` | |
| [.../dxt-lossless-transform-bc3/src/util/bc3\_decode.rs](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/pull/29?src=pr&el=tree&filepath=projects%2Fdxt-lossless-transform-bc3%2Fsrc%2Futil%2Fbc3_decode.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56#diff-cHJvamVjdHMvZHh0LWxvc3NsZXNzLXRyYW5zZm9ybS1iYzMvc3JjL3V0aWwvYmMzX2RlY29kZS5ycw==) | `89.47% <100.00%> (ø)` | |
| [...lossless-transform-common/src/decoded\_4x4\_block.rs](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/pull/29?src=pr&el=tree&filepath=projects%2Fdxt-lossless-transform-common%2Fsrc%2Fdecoded_4x4_block.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56#diff-cHJvamVjdHMvZHh0LWxvc3NsZXNzLXRyYW5zZm9ybS1jb21tb24vc3JjL2RlY29kZWRfNHg0X2Jsb2NrLnJz) | `100.00% <100.00%> (ø)` | |
| [...lossless-transform-bc3/src/normalize\_blocks/mod.rs](https://app.codecov.io/gh/Sewer56/dxt-lossless-transform/pull/29?src=pr&el=tree&filepath=projects%2Fdxt-lossless-transform-bc3%2Fsrc%2Fnormalize_blocks%2Fmod.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Sewer56#diff-cHJvamVjdHMvZHh0LWxvc3NsZXNzLXRyYW5zZm9ybS1iYzMvc3JjL25vcm1hbGl6ZV9ibG9ja3MvbW9kLnJz) | `85.36% <85.36%> (ø)` | |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>
author_association: NONE