package compression_test import ( "context" "fmt" "io/ioutil" "math/rand" "path/filepath" "testing" "time" "github.com/inkyblackness/hacked/ss1/content/movie/internal/compression" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) type CompressionSuite struct { suite.Suite } func TestTileSideLength(t *testing.T) { assert.Equal(t, 4, compression.TileSideLength, "The following tests assume a tile side length of 4") } func TestCompressionTest(t *testing.T) { suite.Run(t, new(CompressionSuite)) } func (suite *CompressionSuite) SetupTest() { } func (suite *CompressionSuite) TestOneFrameOneTile() { frame0 := []byte{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, } suite.verifyCompression(4, 4, frame0) } func (suite *CompressionSuite) TestTwoFramesOneTile() { frame0 := []byte{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, } frame1 := []byte{ 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, } suite.verifyCompression(4, 4, frame0, frame1) } func (suite *CompressionSuite) TestTwoTiles() { frame0 := []byte{ 0x01, 0x02, 0x03, 0x04, 0x20, 0x21, 0x22, 0x23, 0x05, 0x06, 0x07, 0x08, 0x24, 0x25, 0x26, 0x27, 0x09, 0x0A, 0x0B, 0x0C, 0x28, 0x29, 0x2A, 0x2B, 0x0D, 0x0E, 0x0F, 0x10, 0x2C, 0x2D, 0x2E, 0x2F, } suite.verifyCompression(8, 4, frame0) } func (suite *CompressionSuite) verifyCompression(width, height int, inFrames ...[]byte) { suite.T().Helper() verifyCompression(suite.T(), width, height, inFrames...) } func BenchmarkRecompressionDeth00(b *testing.B) { recompress(b, "deth", "scene00") } func BenchmarkRecompressionDeth01(b *testing.B) { recompress(b, "deth", "scene01") } func BenchmarkRecompressionDeth02(b *testing.B) { recompress(b, "deth", "scene02") } func recompress(tb testing.TB, dir1, dir2 string) { tb.Helper() scenepath := filepath.Join(".", "_testdata", dir1, dir2) read := func(name string) []byte { data, err := ioutil.ReadFile(filepath.Join(scenepath, name)) require.Nil(tb, err, "no error expected reading %v", name) return data } controlWordsData := read("controlDict.bin") controlWords, err := compression.UnpackControlWords(controlWordsData) require.Nil(tb, err, "no error expected unpacking control words") paletteLookup := read("paletteLookup.bin") width := 600 height := 300 frame := make([]byte, width*height) decoderBuilder := compression.NewFrameDecoderBuilder(600, 300) logStatistics(tb, "In", controlWords, paletteLookup) decoderBuilder.WithControlWords(controlWords) decoderBuilder.WithPaletteLookupList(paletteLookup) decoderBuilder.ForStandardFrame(frame, width) decoder := decoderBuilder.Build() canDecodeFrame := func(index int) bool { bitstream, err := ioutil.ReadFile(filepath.Join(scenepath, fmt.Sprintf("frame%02d_bitstream.bin", index))) if err != nil { return false } maskstream, err := ioutil.ReadFile(filepath.Join(scenepath, fmt.Sprintf("frame%02d_maskstream.bin", index))) if err != nil { return false } tb.Logf("InStatistics F%2d: Bitstream: %v, Maskstream: %v", index, len(bitstream), len(maskstream)) err = decoder.Decode(bitstream, maskstream) if err != nil { tb.Logf("Failed to decode frame F%2d: %v", index, err) return false } return true } frameIndex := 0 var frames [][]byte for canDecodeFrame(frameIndex) { frameCopy := make([]byte, len(frame)) copy(frameCopy, frame) frames = append(frames, frameCopy) frameIndex++ } tb.Logf("verifying compression of %v frames\n", len(frames)) verifyCompression(tb, width, height, frames...) } func BenchmarkRandomFrames(b *testing.B) { // With a frame size of 600x300 we have 150*75=11250 tiles per frame. // The maximum palette offset is 0x1FFFF, allowing for 8191 entries of full 16 byte (fully random tile) palettes. // This means not even one frame of completely randomized tiles can be encoded. // As a result, this test will first create a set of 4000 random tiles, and then create a sequence of frames // using this set of tiles, instead of creating complete random frames. // Still, this test is rather slow, so it is moved to be a benchmark for now. seed := time.Now().UnixNano() b.Logf("Running with seed %v", seed) hTiles := 150 vTiles := 75 width := 4 * hTiles height := 4 * vTiles r := rand.New(rand.NewSource(seed)) tiles := make([][4][4]byte, 4000) for tileIndex := 0; tileIndex < len(tiles); tileIndex++ { tile := &tiles[tileIndex] for y := 0; y < 4; y++ { for x := 0; x < 4; x++ { tile[y][x] = byte(1 + r.Intn(255)) // value of 0x00 may not be encoded. } } } frames := make([][]byte, 5) for frameIndex := 0; frameIndex < len(frames); frameIndex++ { frame := make([]byte, width*height) for y := 0; y < height; y += 4 { for x := 0; x < width; x += 4 { tile := &tiles[r.Intn(len(tiles))] for line := 0; line < 4; line++ { copy(frame[(y+line)*width+x:], tile[line][:]) } } } frames[frameIndex] = frame } b.ResetTimer() verifyCompression(b, width, height, frames...) } func verifyCompression(tb testing.TB, width, height int, inFrames ...[]byte) { tb.Helper() encoder := compression.NewSceneEncoder(width, height) for frameIndex, frame := range inFrames { require.Equal(tb, width*height, len(frame), fmt.Sprintf("Length of frame %d is wrong for dimension", frameIndex)) err := encoder.AddFrame(frame) require.Nil(tb, err, fmt.Sprintf("no error expected adding frame %d: %v", frameIndex, err)) } controlWords, paletteLookup, encodedFrames, err := encoder.Encode(context.Background()) require.Equal(tb, len(inFrames), len(encodedFrames), "expected equal amount of encoded frames for input frames") require.Nil(tb, err, fmt.Sprintf("no error expected encoding: %v", err)) logStatistics(tb, "Out", controlWords, paletteLookup) decoderBuilder := compression.NewFrameDecoderBuilder(width, height) decoderBuilder.WithControlWords(controlWords) decoderBuilder.WithPaletteLookupList(paletteLookup) frameBuffer := make([]byte, width*height) decoderBuilder.ForStandardFrame(frameBuffer, width) for frameIndex, encodedFrame := range encodedFrames { decoder := decoderBuilder.Build() tb.Logf("Statistics F%2d: Bitstream: %v, Maskstream: %v", frameIndex, len(encodedFrame.Bitstream), len(encodedFrame.Maskstream)) err = decoder.Decode(encodedFrame.Bitstream, encodedFrame.Maskstream) assert.Nil(tb, err, fmt.Sprintf("no error expected re-decoding: %v", err)) assert.Equal(tb, inFrames[frameIndex], frameBuffer, fmt.Sprintf("Frame content mismatch for frame %d", frameIndex)) } } func logStatistics(tb testing.TB, prefix string, controlWords []compression.ControlWord, paletteLookup []byte) { tb.Helper() controls := make(map[compression.ControlType]int) skips := make(map[int]int) longOffsets := 0 for _, word := range controlWords { if word.IsLongOffset() { longOffsets++ } else { skips[word.Count()]++ controls[word.Type()]++ } } firstWords := "" for i := 0; i < 16; i++ { if i < len(controlWords) { word := controlWords[i] if word.IsLongOffset() { firstWords += fmt.Sprintf(" LO: %v", word.LongOffset()) } else { firstWords += fmt.Sprintf(" [%v %v %v]", word.Count(), word.Type(), word.Parameter()) } } } tb.Logf("%vStatistics: PaletteLookup: %vB, ControlWords: %v, LO: %v, CTRL: %v, SKIP: %v -- firstWords: %v", prefix, len(paletteLookup), len(controlWords), longOffsets, controls, skips, firstWords) }