Skip to content

Commit a6306a6

Browse files
authored
Merge pull request #58 from image-rs/fix-2024-12-19-black-white-binary
Fix texel coordinate assignment
2 parents cd28c93 + 0014624 commit a6306a6

File tree

3 files changed

+172
-24
lines changed

3 files changed

+172
-24
lines changed

canvas/src/layout.rs

+4
Original file line numberDiff line numberDiff line change
@@ -848,6 +848,8 @@ impl CanvasLayout {
848848
iter: &[[u32; 2]],
849849
chunk: ChunkSpec,
850850
) {
851+
debug_assert_eq!(idx.len(), iter.len());
852+
851853
if self.texel.bits.bytes() == 0 {
852854
unreachable!("No texel with zero bytes");
853855
}
@@ -873,6 +875,8 @@ impl CanvasLayout {
873875
pitch: u32,
874876
spec: ChunkSpec,
875877
) {
878+
debug_assert_eq!(iter.len(), idx.len());
879+
876880
let pitch = u64::from(pitch);
877881
let mut index_chunks = idx.chunks_mut(spec.chunk_size);
878882
let mut iter = iter.chunks(spec.chunk_size);

canvas/src/shader.rs

+71-24
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,23 @@ use crate::Canvas;
1515

1616
/// A buffer for conversion.
1717
pub struct Converter {
18-
/// How many texels to do at once.
18+
/// How many super-blocks to do at once.
1919
///
20-
/// Each entry in `in_slices` and `out_slices` except for the last has the size of `chunk`.
20+
/// A super-texel is a unit determined by the shader which encompasses a whole number of input
21+
/// and output blocks, i.e. a common multiple of both pixel counts.
2122
chunk: usize,
2223
/// The number of chunks to do at once.
24+
///
25+
/// Each chunk is one consecutive set of super-texels so discontinuities can occur from one
26+
/// chunk to the next. That allows us to specialize the texel index and texel fetch code for
27+
/// the most common texel index schemes that occur as a result.
2328
chunk_count: usize,
2429

30+
/// How many input texels are read in each super-block chunk.
31+
chunk_per_fetch: usize,
32+
/// How many out texels are written in each super-block chunk.
33+
chunk_per_write: usize,
34+
2535
super_blocks: TexelBuffer<[u32; 2]>,
2636
/// Buffer where we store input texels after reading them.
2737
in_texels: TexelBuffer,
@@ -185,9 +195,9 @@ struct IntShuffleOps {
185195
struct SuperTexel {
186196
blocks: Range<u32>,
187197
/// In blocks per super block.
188-
in_super: u32,
198+
in_per_super: u32,
189199
/// Out blocks per super block.
190-
out_super: u32,
200+
out_per_super: u32,
191201
}
192202

193203
pub(crate) struct ChunkSpec<'ch> {
@@ -201,6 +211,8 @@ impl Converter {
201211
Converter {
202212
chunk: 1024,
203213
chunk_count: 1,
214+
chunk_per_fetch: 0,
215+
chunk_per_write: 0,
204216
super_blocks: TexelBuffer::default(),
205217
in_texels: TexelBuffer::default(),
206218
in_coords: TexelBuffer::default(),
@@ -327,9 +339,11 @@ impl Converter {
327339
}
328340

329341
/// Special case on `convert_texelbuf_with_ops`, when both buffers:
330-
/// * utilize an expasion-roundtrip-safe color/bit combination
342+
///
343+
/// * utilize an expansion-roundtrip-safe color/bit combination
331344
/// * have the same bit depths on all channels
332345
/// * do not require any color conversion between them
346+
/// * as a consequence of these, have a common pixel-to-texel ratio of 1-to-1
333347
///
334348
/// This avoids expanding them into `pixel_in_buffer` where they'd be represented as `f32x4`
335349
/// and thus undergo an expensive `u8->f32->u8` cast chain.
@@ -390,6 +404,16 @@ impl Converter {
390404
) where
391405
T: AsTexel,
392406
{
407+
debug_assert_eq!(
408+
that.chunk, that.chunk_per_fetch,
409+
"Inconsistent usage of channel shuffle, only applicable to matching texels"
410+
);
411+
412+
debug_assert_eq!(
413+
that.chunk, that.chunk_per_write,
414+
"Inconsistent usage of channel shuffle, only applicable to matching texels"
415+
);
416+
393417
let in_texel = T::texel().array::<N>();
394418
let out_texel = T::texel().array::<M>();
395419

@@ -527,6 +551,10 @@ impl Converter {
527551
frame_in: &Canvas,
528552
frame_out: &mut Canvas,
529553
) {
554+
// We *must* make progress.
555+
assert!(self.chunk > 0);
556+
assert!(self.chunk_count > 0);
557+
530558
use core::slice::from_mut;
531559
// We use a notion of 'supertexels', the common multiple of input and output texel blocks.
532560
// That is, if the input is a 2-by-2 pixel block and the output is single pixels then we
@@ -536,6 +564,17 @@ impl Converter {
536564
let (sb_x, sb_y) = self.super_texel(info);
537565
let mut blocks = Self::blocks(sb_x.blocks.clone(), sb_y.blocks.clone());
538566

567+
assert!(sb_x.in_per_super > 0);
568+
assert!(sb_x.in_per_super > 0);
569+
assert!(sb_x.out_per_super > 0);
570+
assert!(sb_y.out_per_super > 0);
571+
572+
self.chunk_per_fetch = self.chunk * (sb_x.in_per_super * sb_y.in_per_super) as usize;
573+
self.chunk_per_write = self.chunk * (sb_x.out_per_super * sb_y.out_per_super) as usize;
574+
575+
assert!(self.chunk_per_fetch > 0);
576+
assert!(self.chunk_per_write > 0);
577+
539578
loop {
540579
let at_once = self.chunk * self.chunk_count;
541580
self.super_blocks.resize(at_once);
@@ -566,6 +605,8 @@ impl Converter {
566605

567606
let super_width = core::cmp::max(b0.width(), b1.width());
568607
let super_height = core::cmp::max(b0.height(), b1.height());
608+
609+
// All currently supported texels are a power-of-two.
569610
assert!(super_width % b0.width() == 0);
570611
assert!(super_width % b1.width() == 0);
571612
assert!(super_height % b0.height() == 0);
@@ -579,13 +620,13 @@ impl Converter {
579620
(
580621
SuperTexel {
581622
blocks: 0..sb_height,
582-
in_super: super_height / b0.height(),
583-
out_super: super_height / b1.height(),
623+
in_per_super: super_height / b0.height(),
624+
out_per_super: super_height / b1.height(),
584625
},
585626
SuperTexel {
586627
blocks: 0..sb_width,
587-
in_super: super_width / b0.width(),
588-
out_super: super_width / b1.width(),
628+
in_per_super: super_width / b0.width(),
629+
out_per_super: super_width / b1.width(),
589630
},
590631
)
591632
}
@@ -599,7 +640,7 @@ impl Converter {
599640
sb_y: &SuperTexel,
600641
) {
601642
fn is_trivial_super(sup: &SuperTexel) -> bool {
602-
sup.in_super == 1 && sup.out_super == 1
643+
sup.in_per_super == 1 && sup.out_per_super == 1
603644
}
604645

605646
self.in_coords.resize(0);
@@ -620,10 +661,10 @@ impl Converter {
620661
.as_mut_slice()
621662
.copy_from_slice(&self.super_blocks);
622663
} else {
623-
let in_chunk_len = (sb_x.in_super * sb_y.in_super) as usize;
664+
let in_chunk_len = (sb_x.in_per_super * sb_y.in_per_super) as usize;
624665
self.in_coords
625666
.resize(self.super_blocks.len() * in_chunk_len);
626-
let out_chunk_len = (sb_x.out_super * sb_y.out_super) as usize;
667+
let out_chunk_len = (sb_x.out_per_super * sb_y.out_per_super) as usize;
627668
self.out_coords
628669
.resize(self.super_blocks.len() * out_chunk_len);
629670

@@ -637,18 +678,18 @@ impl Converter {
637678
.chunks_exact_mut(out_chunk_len);
638679

639680
for &[bx, by] in self.super_blocks.as_slice().iter() {
640-
let (sx, sy) = (bx * sb_x.in_super, by * sb_y.in_super);
681+
let (sx, sy) = (bx * sb_x.in_per_super, by * sb_y.in_per_super);
641682
if let Some(chunk) = in_chunks.next() {
642-
Self::blocks(0..sb_x.in_super, 0..sb_y.in_super)(chunk);
683+
Self::blocks(0..sb_x.in_per_super, 0..sb_y.in_per_super)(chunk);
643684
for p in chunk.iter_mut() {
644685
let [ix, iy] = *p;
645686
*p = [sx + ix, sy + iy];
646687
}
647688
}
648689

649-
let (sx, sy) = (bx * sb_x.out_super, by * sb_y.out_super);
690+
let (sx, sy) = (bx * sb_x.out_per_super, by * sb_y.out_per_super);
650691
if let Some(chunk) = out_chunks.next() {
651-
Self::blocks(0..sb_x.out_super, 0..sb_y.out_super)(chunk);
692+
Self::blocks(0..sb_x.out_per_super, 0..sb_y.out_per_super)(chunk);
652693
for p in chunk.iter_mut() {
653694
let [ox, oy] = *p;
654695
*p = [sx + ox, sy + oy];
@@ -665,13 +706,13 @@ impl Converter {
665706

666707
let in_chunk = ChunkSpec {
667708
chunks: self.in_slices.as_mut_slice(),
668-
chunk_size: self.chunk,
709+
chunk_size: self.chunk_per_fetch,
669710
should_defer_texel_ops: converter.should_defer_texel_read,
670711
};
671712

672713
let out_chunk = ChunkSpec {
673714
chunks: self.out_slices.as_mut_slice(),
674-
chunk_size: self.chunk,
715+
chunk_size: self.chunk_per_write,
675716
should_defer_texel_ops: converter.should_defer_texel_write,
676717
};
677718

@@ -681,6 +722,7 @@ impl Converter {
681722
&mut self.in_index_list,
682723
in_chunk,
683724
);
725+
684726
(ops.fill_out_index)(
685727
&info,
686728
self.out_coords.as_slice(),
@@ -778,8 +820,8 @@ impl Converter {
778820
* `in_texels`.
779821
*/
780822
let chunks = self.in_slices.as_mut_slice();
781-
let indexes = self.in_index_list.chunks(self.chunk);
782-
let range = (0..self.in_index_list.len()).step_by(self.chunk);
823+
let indexes = self.in_index_list.chunks(self.chunk_per_fetch);
824+
let range = (0..self.in_index_list.len()).step_by(self.chunk_per_fetch);
783825

784826
for (chunk, (indexes, start)) in chunks.iter_mut().zip(indexes.zip(range)) {
785827
let [_, available] = chunk;
@@ -805,7 +847,7 @@ impl Converter {
805847
idx: &self.in_index_list,
806848
into: &mut self.in_texels,
807849
range: 0..self.in_index_list.len(),
808-
})
850+
});
809851
}
810852
}
811853

@@ -831,6 +873,9 @@ impl Converter {
831873
let texels = &from.as_texels(texel)[range];
832874
let texel_slice = into.as_mut_texels(texel);
833875

876+
// The index structure and used texel type should match.
877+
debug_assert_eq!(idx.len(), texels.len());
878+
834879
for (&index, from) in idx.zip(texels) {
835880
if let Some(into) = texel_slice.get_mut(index) {
836881
*into = texel.copy_val(from);
@@ -859,8 +904,8 @@ impl Converter {
859904
* the `out_texels`.
860905
*/
861906
let chunks = self.out_slices.as_slice();
862-
let indexes = self.out_index_list.chunks(self.chunk);
863-
let range = (0..self.out_index_list.len()).step_by(self.chunk);
907+
let indexes = self.out_index_list.chunks(self.chunk_per_write);
908+
let range = (0..self.out_index_list.len()).step_by(self.chunk_per_write);
864909

865910
for (&chunk, (indexes, start)) in chunks.iter().zip(indexes.zip(range)) {
866911
let [_, unwritten] = chunk;
@@ -1372,7 +1417,7 @@ impl CommonPixel {
13721417
// target buffer by chunks if this is available.
13731418
fn join_bits<const N: usize>(
13741419
info: &Info,
1375-
ops: &ConvertOps,
1420+
_: &ConvertOps,
13761421
bits: [[FromBits; 4]; N],
13771422
pixel_buf: &TexelBuffer,
13781423
out_texels: &mut TexelBuffer,
@@ -1413,6 +1458,8 @@ impl CommonPixel {
14131458
let texel_slice = self.out_texels.as_mut_texels(texel);
14141459
let pixel_slice = self.pixel_buf.as_texels(self.join.array::<N>());
14151460

1461+
debug_assert_eq!(texel_slice.len(), pixel_slice.len());
1462+
14161463
for ch in [0u8, 1, 2, 3] {
14171464
for (texbits, pixels) in texel_slice.iter_mut().zip(pixel_slice) {
14181465
for (pixel_bits, joined) in self.bits.iter().zip(pixels) {

canvas/src/tests.rs

+97
Original file line numberDiff line numberDiff line change
@@ -463,3 +463,100 @@ fn incorrect_strided() {
463463

464464
assert!(layout.is_err());
465465
}
466+
467+
/// Verify some bit packing code by manually constructed matching pairs, with partial texel blocks.
468+
#[test]
469+
fn a_bitpack_dilemma_wrapped() {
470+
let from = Texel::new_u8(SampleParts::Luma);
471+
472+
let into = Texel {
473+
block: Block::Pack1x8,
474+
bits: SampleBits::UInt1x8,
475+
parts: SampleParts::Luma,
476+
};
477+
478+
let mut from = Canvas::new(CanvasLayout::with_texel(&from, 23, 4).unwrap());
479+
let mut into = Canvas::new(CanvasLayout::with_texel(&into, 23, 4).unwrap());
480+
481+
#[rustfmt::skip]
482+
const IMG_BW_LUMA: &[u8; 23 * 4] = &[
483+
0, 0, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0,
484+
0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0,
485+
0, 0, 0, 0, 0, 0, 0, 128, 8, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 128,
486+
0, 0, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0,
487+
];
488+
489+
#[rustfmt::skip]
490+
const IMG_BW_PACKED: &[u8; 3 * 4] = &[
491+
0x00, 0xff, 0x00,
492+
0x00, 0x00, 0x00,
493+
0x01, 0x00, 0x02,
494+
0x00, 0xff, 0x00,
495+
];
496+
497+
from.as_bytes_mut().copy_from_slice(IMG_BW_LUMA);
498+
from.convert(&mut into);
499+
500+
assert_eq!(into.as_bytes(), IMG_BW_PACKED);
501+
}
502+
503+
/// Verify some bit packing code by manually constructed matching pairs, with full texel blocks.
504+
#[test]
505+
fn a_bitpack_dilemma_unwrapped() {
506+
let texel_from = Texel::new_u8(SampleParts::Luma);
507+
508+
let texel_into = Texel {
509+
block: Block::Pack1x8,
510+
bits: SampleBits::UInt1x8,
511+
parts: SampleParts::Luma,
512+
};
513+
514+
let mut from = Canvas::new(CanvasLayout::with_texel(&texel_from, 24, 9).unwrap());
515+
let mut into = Canvas::new(CanvasLayout::with_texel(&texel_into, 24, 9).unwrap());
516+
517+
#[rustfmt::skip]
518+
const IMG_BW_LUMA: &[u8; 24 * 9] = &[
519+
0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 128,
520+
0, 0, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 128,
521+
0, 0, 0, 0, 0, 0, 0, 128, 8, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 128, 128,
522+
0, 0, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 0, 128,
523+
524+
0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 128,
525+
0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 128,
526+
0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 128,
527+
0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8, 8, 8, 8, 8, 0, 0, 0, 0, 0, 0, 0, 128,
528+
529+
0, 0, 0, 0, 0, 0, 0, 0, 128, 128, 128, 128, 128, 128, 128, 128, 0, 0, 0, 0, 0, 0, 128, 128,
530+
];
531+
532+
#[rustfmt::skip]
533+
const IMG_BW_PACKED: &[u8; 3 * 9] = &[
534+
0x00, 0x00, 0x01,
535+
0x00, 0xff, 0x01,
536+
0x01, 0x00, 0x03,
537+
0x00, 0xff, 0x01,
538+
539+
0x00, 0x00, 0x01,
540+
0x00, 0x00, 0x01,
541+
0x00, 0x00, 0x01,
542+
0x00, 0x00, 0x01,
543+
544+
0x00, 0xff, 0x03,
545+
];
546+
547+
from.as_bytes_mut().copy_from_slice(IMG_BW_LUMA);
548+
from.convert(&mut into);
549+
550+
assert_eq!(into.as_bytes(), IMG_BW_PACKED);
551+
552+
for i in (0..48).step_by(1) {
553+
let mut from = Canvas::new(CanvasLayout::with_texel(&texel_from, 24, 9 + i).unwrap());
554+
let mut into = Canvas::new(CanvasLayout::with_texel(&texel_into, 24, 9 + i).unwrap());
555+
let i = i as usize;
556+
557+
from.as_bytes_mut()[24 * i..].copy_from_slice(IMG_BW_LUMA);
558+
from.convert(&mut into);
559+
560+
assert_eq!(&into.as_bytes()[3 * i..], IMG_BW_PACKED, "{i}");
561+
}
562+
}

0 commit comments

Comments
 (0)