Adding modding folder with a hacked build.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
package compression
|
||||
|
||||
// BitstreamReader is a utility to read big-endian integer values of arbitrary bit size from a bitstream.
|
||||
type BitstreamReader struct {
|
||||
source []byte
|
||||
nextSourceIndex int
|
||||
sourceBitLen uint64
|
||||
|
||||
currentBitPos uint64
|
||||
|
||||
buffer uint64
|
||||
bitsBuffered uint64
|
||||
}
|
||||
|
||||
// NewBitstreamReader returns a new instance of a bistream reader using the provided byte slice as source.
|
||||
func NewBitstreamReader(data []byte) *BitstreamReader {
|
||||
return &BitstreamReader{
|
||||
source: data,
|
||||
sourceBitLen: uint64(len(data)) * 8,
|
||||
}
|
||||
}
|
||||
|
||||
func (reader *BitstreamReader) bufferNextByte() {
|
||||
reader.buffer = (reader.buffer << 8) | uint64(reader.source[reader.nextSourceIndex])
|
||||
reader.bitsBuffered += 8
|
||||
reader.nextSourceIndex++
|
||||
}
|
||||
|
||||
// Exhausted returns true if the current position is at or beyond the possible length.
|
||||
// Reading an exhausted stream yields only zeroes.
|
||||
func (reader *BitstreamReader) Exhausted() bool {
|
||||
return reader.currentBitPos >= reader.sourceBitLen
|
||||
}
|
||||
|
||||
// Read returns a value with the requested bit size, right aligned, as a uint32.
|
||||
// Reading does not advance the current position. A successful read of a certain size will return the same
|
||||
// value when called repeatedly with the same parameter.
|
||||
// Reading is possible beyond the available size. For the missing bits, a value of 0 is provided.
|
||||
//
|
||||
// The function panics when reading more than 32 bits.
|
||||
func (reader *BitstreamReader) Read(bits int) (result uint32) {
|
||||
if bits > 32 {
|
||||
panic("Limit of bit count: 32")
|
||||
}
|
||||
|
||||
available := reader.sourceBitLen - reader.currentBitPos
|
||||
toRead := uint64(bits)
|
||||
if toRead > available {
|
||||
toRead = available
|
||||
}
|
||||
|
||||
for reader.bitsBuffered < toRead {
|
||||
reader.bufferNextByte()
|
||||
}
|
||||
result = uint32(reader.buffer >> (reader.bitsBuffered - toRead) & ^(uint64(0xFFFFFFFFFFFFFFFF) << toRead))
|
||||
result <<= uint64(bits) - toRead
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Advance skips the provided amount of bits and puts the current position there.
|
||||
// Successive read operations will return values from the new position.
|
||||
// It is possible to advance beyond the available length.
|
||||
//
|
||||
// This function panics when advancing with negative values.
|
||||
func (reader *BitstreamReader) Advance(bits int) {
|
||||
if bits < 0 {
|
||||
panic("Can only advance forward")
|
||||
}
|
||||
|
||||
newIndex := reader.currentBitPos + uint64(bits)
|
||||
if newIndex > reader.sourceBitLen {
|
||||
newIndex = reader.sourceBitLen
|
||||
}
|
||||
reader.currentBitPos = newIndex
|
||||
if uint64(bits) > reader.bitsBuffered {
|
||||
reader.bitsBuffered = 0
|
||||
reader.nextSourceIndex = int(reader.currentBitPos / 8)
|
||||
remainder := reader.currentBitPos % 8
|
||||
if remainder > 0 {
|
||||
reader.bufferNextByte()
|
||||
reader.bitsBuffered -= remainder
|
||||
}
|
||||
} else {
|
||||
reader.bitsBuffered -= uint64(bits)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/compression"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBitstreamReadPanicsForMoreThan32Bits(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0x11, 0x22, 0x33, 0x44})
|
||||
|
||||
assert.Panicsf(t, func() { reader.Read(33) }, "Limit of bit count: 32")
|
||||
}
|
||||
|
||||
func TestBitstreamReadReturnsValueOfRequestedBitSize(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xAF})
|
||||
|
||||
result := reader.Read(3)
|
||||
|
||||
assert.Equal(t, uint32(5), result)
|
||||
}
|
||||
|
||||
func TestBitstreamRepeatedReadReturnsSameValue(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xAF})
|
||||
|
||||
result1 := reader.Read(3)
|
||||
result2 := reader.Read(3)
|
||||
|
||||
assert.Equal(t, uint32(5), result1)
|
||||
assert.Equal(t, result1, result2)
|
||||
}
|
||||
|
||||
func TestBitstreamReadReturnsZeroesForBitsBeyondEndA(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xAF})
|
||||
|
||||
result := reader.Read(9)
|
||||
|
||||
assert.Equal(t, uint32(0x15E), result)
|
||||
}
|
||||
|
||||
func TestBitstreamReadReturnsZeroesForBitsBeyondEndB(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xBF})
|
||||
|
||||
result := reader.Read(32)
|
||||
|
||||
assert.Equal(t, uint32(0xBF000000), result)
|
||||
}
|
||||
|
||||
func TestBitstreamAdvancePanicsForNegativeValues(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0x11, 0x22, 0x33, 0x44})
|
||||
|
||||
assert.Panicsf(t, func() { reader.Advance(-10) }, "Can only advance forward")
|
||||
}
|
||||
|
||||
func TestBitstreamAdvanceLetsReadFurtherBits(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xAF})
|
||||
|
||||
reader.Advance(2)
|
||||
result := reader.Read(4)
|
||||
|
||||
assert.Equal(t, uint32(0x0B), result)
|
||||
}
|
||||
|
||||
func TestBitstreamAdvanceToEndIsPossible(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xAF})
|
||||
|
||||
reader.Advance(8)
|
||||
result := reader.Read(8)
|
||||
|
||||
assert.Equal(t, uint32(0), result)
|
||||
}
|
||||
|
||||
func TestBitstreamInternalBufferDoesNotLoseData(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0x7F, 0xFF, 0xFF, 0xFF, 0x80})
|
||||
|
||||
reader.Advance(1)
|
||||
result := reader.Read(32)
|
||||
|
||||
assert.Equal(t, uint32(0xFFFFFFFF), result)
|
||||
}
|
||||
|
||||
func TestBitstreamAdvanceCanJumpToLastBit(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0x00, 0x00, 0x01})
|
||||
|
||||
reader.Advance(23)
|
||||
result := reader.Read(1)
|
||||
|
||||
assert.Equal(t, uint32(1), result)
|
||||
}
|
||||
|
||||
func TestBitstreamReadAdvanceBeyondFirstRead(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xFF, 0x00, 0xFA})
|
||||
|
||||
reader.Read(10)
|
||||
reader.Advance(20)
|
||||
result := reader.Read(4)
|
||||
|
||||
assert.Equal(t, uint32(0x0A), result)
|
||||
}
|
||||
|
||||
func TestBitstreamReadAdvanceWithinFirstRead(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xFF, 0x00, 0xFA})
|
||||
|
||||
reader.Read(10)
|
||||
reader.Advance(4)
|
||||
result := reader.Read(20)
|
||||
|
||||
assert.Equal(t, uint32(0xF00FA), result)
|
||||
}
|
||||
|
||||
func TestBitstreamReadOfZeroBitsIsPossibleMidStream(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xFF})
|
||||
|
||||
reader.Read(4)
|
||||
result := reader.Read(0)
|
||||
|
||||
assert.Equal(t, uint32(0), result)
|
||||
}
|
||||
|
||||
func TestBitstreamReadOfZeroBitsIsPossibleAtEnd(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xFF})
|
||||
|
||||
reader.Advance(8)
|
||||
result := reader.Read(0)
|
||||
|
||||
assert.Equal(t, uint32(0), result)
|
||||
}
|
||||
|
||||
func TestBitstreamReadOfZeroBitsIsPossibleWithEmptySource(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{})
|
||||
|
||||
result := reader.Read(0)
|
||||
|
||||
assert.Equal(t, uint32(0), result)
|
||||
}
|
||||
|
||||
func TestBitstreamAdvanceBeyondEndIsPossible(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xFF})
|
||||
|
||||
reader.Advance(9)
|
||||
result := reader.Read(32)
|
||||
|
||||
assert.Equal(t, uint32(0), result)
|
||||
}
|
||||
|
||||
func TestBitstreamExhaustedReturnsFalseForAvailableBits(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xFF})
|
||||
|
||||
result := reader.Exhausted()
|
||||
|
||||
assert.False(t, result)
|
||||
}
|
||||
|
||||
func TestBitstreamExhaustedReturnsFalseForStillOneAvailableBit(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xFF})
|
||||
|
||||
reader.Advance(7)
|
||||
result := reader.Exhausted()
|
||||
|
||||
assert.False(t, result)
|
||||
}
|
||||
|
||||
func TestBitstreamExhaustedReturnsTrueAfterAdvancingToEnd(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xFF, 0xFF})
|
||||
|
||||
reader.Advance(16)
|
||||
result := reader.Exhausted()
|
||||
|
||||
assert.True(t, result)
|
||||
}
|
||||
|
||||
func TestBitstreamExhaustedReturnsTrueAfterAdvancingBeyondEnd(t *testing.T) {
|
||||
reader := compression.NewBitstreamReader([]byte{0xFF, 0xFF})
|
||||
|
||||
reader.Advance(30)
|
||||
result := reader.Exhausted()
|
||||
|
||||
assert.True(t, result)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package compression
|
||||
|
||||
// BitstreamWriter is a utility to write big-endian integer values of arbitrary bit size to a bitstream.
|
||||
type BitstreamWriter struct {
|
||||
buf []byte
|
||||
|
||||
offset uint
|
||||
}
|
||||
|
||||
// Buffer returns the currently stored buffer.
|
||||
func (w BitstreamWriter) Buffer() []byte {
|
||||
return w.buf
|
||||
}
|
||||
|
||||
// Write stores the given amount of bits from the provided value.
|
||||
// Trying to write more than 32 bits will cause panic.
|
||||
func (w *BitstreamWriter) Write(bits uint, value uint32) {
|
||||
if bits > 32 {
|
||||
panic("maximum of 32 bits are possible to be written")
|
||||
}
|
||||
for bits > 0 {
|
||||
temp := (uint64(value) & ^(^uint64(0) << bits)) << (40 - bits - w.offset)
|
||||
if w.offset == 0 {
|
||||
w.buf = append(w.buf, 0x00)
|
||||
}
|
||||
w.buf[len(w.buf)-1] |= byte(temp >> 32)
|
||||
written := 8 - w.offset
|
||||
if written > bits {
|
||||
written = bits
|
||||
}
|
||||
bits -= written
|
||||
w.offset = (w.offset + written) % 8
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/compression"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type BitstreamWriterSuite struct {
|
||||
suite.Suite
|
||||
|
||||
writer compression.BitstreamWriter
|
||||
}
|
||||
|
||||
func TestBitstreamWriter(t *testing.T) {
|
||||
suite.Run(t, new(BitstreamWriterSuite))
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) SetupTest() {
|
||||
suite.writer = compression.BitstreamWriter{}
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) TestWriteMoreThan32BitsPanics() {
|
||||
assert.Panics(suite.T(), func() { suite.whenWriting(33, 0) })
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) TestWriteZeroBitsIsIgnored() {
|
||||
suite.whenWriting(0, 1)
|
||||
suite.thenBufferShouldBe([]byte{})
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) TestWriteOneBit() {
|
||||
suite.whenWriting(1, 1)
|
||||
suite.thenBufferShouldBe([]byte{0x80})
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) TestWriteEightBits() {
|
||||
suite.whenWriting(8, 0xEF)
|
||||
suite.thenBufferShouldBe([]byte{0xEF})
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) TestWriteTwelveBits() {
|
||||
suite.whenWriting(12, 0xFEF)
|
||||
suite.thenBufferShouldBe([]byte{0xFE, 0xF0})
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) TestWriteOneBitTwice() {
|
||||
suite.givenWritten(1, 1)
|
||||
suite.whenWriting(1, 1)
|
||||
suite.thenBufferShouldBe([]byte{0xC0})
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) TestWriteArbitrary1() {
|
||||
suite.givenWritten(1, 1)
|
||||
suite.givenWritten(10, 0x2AA)
|
||||
suite.whenWriting(16, 0xFFFF)
|
||||
suite.thenBufferShouldBe([]byte{0xD5, 0x5F, 0xFF, 0xE0}) // [1 10 10101] [010 11111] [11111111] [111 00000]
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) TestWriteArbitrary2() {
|
||||
suite.givenWritten(1, 1)
|
||||
suite.givenWritten(10, 0x2AA)
|
||||
suite.whenWriting(24, 0xFFFFFF)
|
||||
suite.thenBufferShouldBe([]byte{0xD5, 0x5F, 0xFF, 0xFF, 0xE0}) // extension of Arbitrary1
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) TestWriteLessBitsThanGiven() {
|
||||
suite.givenWritten(4, 0x0)
|
||||
suite.whenWriting(8, 0xFFF)
|
||||
suite.thenBufferShouldBe([]byte{0x0F, 0xF0})
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) TestWriteArbitrary3() {
|
||||
suite.givenWritten(1, 1)
|
||||
suite.givenWritten(10, 0x2AA)
|
||||
suite.whenWriting(32, 0xFFFFFFFF)
|
||||
suite.thenBufferShouldBe([]byte{0xD5, 0x5F, 0xFF, 0xFF, 0xFF, 0xE0}) // extension of Arbitrary1+2
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) givenWritten(bits uint, value uint32) {
|
||||
suite.writer.Write(bits, value)
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) whenWriting(bits uint, value uint32) {
|
||||
suite.writer.Write(bits, value)
|
||||
}
|
||||
|
||||
func (suite *BitstreamWriterSuite) thenBufferShouldBe(expected []byte) {
|
||||
suite.T().Helper()
|
||||
buf := suite.writer.Buffer()
|
||||
if len(expected) == 0 {
|
||||
assert.Equal(suite.T(), 0, len(buf))
|
||||
} else {
|
||||
assert.Equal(suite.T(), expected, buf)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package compression
|
||||
|
||||
const (
|
||||
bytesPerControlWord = 3
|
||||
|
||||
// TileSideLength is the number of pixel per side of a square frame tile.
|
||||
TileSideLength = 4
|
||||
// PixelPerTile is the number of pixel within one frame tile.
|
||||
PixelPerTile = TileSideLength * TileSideLength
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
package compression
|
||||
|
||||
// ControlType defines how to interpret a ControlWord.
|
||||
type ControlType byte
|
||||
|
||||
// Control type constants are listed below.
|
||||
const (
|
||||
CtrlColorTile2ColorsStatic = ControlType(0)
|
||||
CtrlColorTile2ColorsMasked = ControlType(1)
|
||||
CtrlColorTile4ColorsMasked = ControlType(2)
|
||||
CtrlColorTile8ColorsMasked = ControlType(3)
|
||||
CtrlColorTile16ColorsMasked = ControlType(4)
|
||||
|
||||
CtrlSkip = ControlType(5)
|
||||
|
||||
CtrlRepeatPrevious = ControlType(6)
|
||||
CtrlUnknown = ControlType(7)
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
package compression
|
||||
|
||||
// ControlWordParamLimit is the highest number the param property of a control word can take.
|
||||
const ControlWordParamLimit = 0x1FFFF
|
||||
|
||||
// ControlWord describes the current compression action.
|
||||
type ControlWord uint32
|
||||
|
||||
// ControlWordOf returns a word with the given parameters.
|
||||
func ControlWordOf(count int, controlType ControlType, param uint32) ControlWord {
|
||||
return (ControlWord(count) << 20) | (ControlWord(controlType&0x7) << 17) | ControlWord(param&ControlWordParamLimit)
|
||||
}
|
||||
|
||||
// LongOffsetOf returns a word that indicates a long offset with given value.
|
||||
func LongOffsetOf(offset uint32) ControlWord {
|
||||
return ControlWord(offset & 0xFFFFF)
|
||||
}
|
||||
|
||||
// Packed returns the control word packed in a PackedControlWord with the given number of times.
|
||||
func (word ControlWord) Packed(times int) PackedControlWord {
|
||||
return PackedControlWord((uint32(word) & 0x00FFFFFF) | (uint32(times) << 24))
|
||||
}
|
||||
|
||||
// Count returns the count value of the control.
|
||||
func (word ControlWord) Count() int {
|
||||
return int((uint32(word) >> 20) & 0xF)
|
||||
}
|
||||
|
||||
// IsLongOffset returns true if Count() returns 0.
|
||||
func (word ControlWord) IsLongOffset() bool {
|
||||
return word.Count() == 0
|
||||
}
|
||||
|
||||
// LongOffset returns the long offset value. Only relevant if IsLongOffset() returns true.
|
||||
func (word ControlWord) LongOffset() uint32 {
|
||||
return (uint32(word) >> 0) & 0xFFFFF
|
||||
}
|
||||
|
||||
// Type returns the type of the control. Only relevant if IsLongOffset() returns false.
|
||||
func (word ControlWord) Type() ControlType {
|
||||
return ControlType((uint32(word) >> 17) & 0x7)
|
||||
}
|
||||
|
||||
// Parameter returns the type specific parameter value.
|
||||
func (word ControlWord) Parameter() uint32 {
|
||||
return (uint32(word) >> 0) & ControlWordParamLimit
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package compression
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1"
|
||||
)
|
||||
|
||||
const (
|
||||
errTooHighOperationOffset ss1.StringError = "too high operation offset"
|
||||
)
|
||||
|
||||
// TileColorOp describes one operation how a tile should be colored.
|
||||
type TileColorOp struct {
|
||||
Type ControlType
|
||||
Offset uint32
|
||||
}
|
||||
|
||||
// ControlWordSequencer receives a list of requested tile coloring operations
|
||||
// and produces a sequence of low-level control words to reproduce the requested list.
|
||||
type ControlWordSequencer struct {
|
||||
// BitstreamIndexLimit specifies the highest value the sequencer may use to store offset values in the bitstream.
|
||||
// If this value is 0, then the default of 0xFFF (12 bits) is used.
|
||||
BitstreamIndexLimit uint32
|
||||
|
||||
// DirectIndexLimit specifies the amount of direct pointers into the bitstream.
|
||||
// If this value is 0, then the default of BitstreamIndexLimit / 4 is used.
|
||||
DirectIndexLimit uint32
|
||||
|
||||
ops map[TileColorOp]uint32
|
||||
}
|
||||
|
||||
// Add extends the list of requested coloring operations with the given entry.
|
||||
// The offset of the entry must be a value less than or equal ControlWordParamLimit, otherwise the function returns an error.
|
||||
func (seq *ControlWordSequencer) Add(op TileColorOp) error {
|
||||
if op.Offset > ControlWordParamLimit {
|
||||
return errTooHighOperationOffset
|
||||
}
|
||||
if seq.ops == nil {
|
||||
seq.ops = make(map[TileColorOp]uint32)
|
||||
}
|
||||
seq.ops[op]++
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sequence packs the requested list of entries into a sequence of control words.
|
||||
// An error is returned if the operations would exceed the possible storage space.
|
||||
func (seq ControlWordSequencer) Sequence() (ControlWordSequence, error) {
|
||||
var result ControlWordSequence
|
||||
var sortedOps []TileColorOp
|
||||
for op := range seq.ops {
|
||||
sortedOps = append(sortedOps, op)
|
||||
}
|
||||
sort.Slice(sortedOps, func(a, b int) bool {
|
||||
opA := sortedOps[a]
|
||||
opB := sortedOps[b]
|
||||
countA := seq.ops[opA]
|
||||
countB := seq.ops[opB]
|
||||
if countA != countB {
|
||||
return countA > countB
|
||||
}
|
||||
if opA.Offset != opB.Offset {
|
||||
return opA.Offset < opB.Offset
|
||||
}
|
||||
if opA.Type != opB.Type {
|
||||
return opA.Type < opB.Type
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
bitstreamIndexLimit := seq.BitstreamIndexLimit
|
||||
if bitstreamIndexLimit == 0 {
|
||||
bitstreamIndexLimit = 0xFFF
|
||||
}
|
||||
maximumDirect := seq.DirectIndexLimit
|
||||
if maximumDirect > bitstreamIndexLimit {
|
||||
maximumDirect = bitstreamIndexLimit
|
||||
} else if maximumDirect == 0 {
|
||||
maximumDirect = bitstreamIndexLimit / 4
|
||||
}
|
||||
requiredExtensions := uint32(0)
|
||||
|
||||
totalOps := uint32(len(sortedOps))
|
||||
if totalOps > maximumDirect {
|
||||
requiredExtensions = (totalOps - maximumDirect + 15) / 16
|
||||
if requiredExtensions > (bitstreamIndexLimit - maximumDirect) {
|
||||
return result, tooManyWordsError{
|
||||
Total: totalOps,
|
||||
MaximumDirect: maximumDirect,
|
||||
BitstreamIndexLimit: bitstreamIndexLimit,
|
||||
}
|
||||
}
|
||||
}
|
||||
extensionStart := maximumDirect + requiredExtensions
|
||||
|
||||
result.opPaths = make(map[TileColorOp]nestedTileColorOp)
|
||||
for opIndex, op := range sortedOps {
|
||||
if uint32(opIndex) == maximumDirect {
|
||||
// write required amount of long offset entries
|
||||
for i := uint32(0); i < requiredExtensions; i++ {
|
||||
result.words = append(result.words, LongOffsetOf(extensionStart+i*16))
|
||||
}
|
||||
}
|
||||
if uint32(opIndex) >= maximumDirect {
|
||||
offset := uint32(opIndex) - maximumDirect
|
||||
result.opPaths[op] = nestedTileColorOp{
|
||||
parent: &nestedTileColorOp{relOffsetBits: 12, relOffset: maximumDirect + offset/16},
|
||||
relOffsetBits: 4,
|
||||
relOffset: offset % 16,
|
||||
}
|
||||
result.words = append(result.words, ControlWordOf(4, op.Type, op.Offset))
|
||||
} else {
|
||||
result.opPaths[op] = nestedTileColorOp{parent: nil, relOffsetBits: 12, relOffset: uint32(opIndex)}
|
||||
result.words = append(result.words, ControlWordOf(12, op.Type, op.Offset))
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type nestedTileColorOp struct {
|
||||
parent *nestedTileColorOp
|
||||
relOffsetBits uint
|
||||
relOffset uint32
|
||||
}
|
||||
|
||||
func (op nestedTileColorOp) writeTo(w *BitstreamWriter) {
|
||||
if op.parent != nil {
|
||||
op.parent.writeTo(w)
|
||||
}
|
||||
w.Write(op.relOffsetBits, op.relOffset)
|
||||
}
|
||||
|
||||
// ControlWordSequence is a finalized set of control words to reproduce a list of
|
||||
// tile coloring operations. Based on this sequence, a bitstream can be created based
|
||||
// on a selection of such coloring operations (i.e., per frame).
|
||||
type ControlWordSequence struct {
|
||||
// HTiles specifies the amount of horizontal tiles (= operations) a frame has.
|
||||
// This number is relevant for skip operations. If 0, then no compression of skip-operations is done.
|
||||
HTiles uint32
|
||||
|
||||
words []ControlWord
|
||||
opPaths map[TileColorOp]nestedTileColorOp
|
||||
}
|
||||
|
||||
// ControlWords returns the list of low-level control words of the sequence.
|
||||
func (seq ControlWordSequence) ControlWords() []ControlWord {
|
||||
return seq.words
|
||||
}
|
||||
|
||||
// BitstreamFor returns the bitstream to reproduce the provided list of coloring operations
|
||||
// from this sequence.
|
||||
func (seq ControlWordSequence) BitstreamFor(ops []TileColorOp) ([]byte, error) {
|
||||
var writer BitstreamWriter
|
||||
writeOp := func(op TileColorOp) {
|
||||
nested := seq.opPaths[op]
|
||||
nested.writeTo(&writer)
|
||||
}
|
||||
pendingSkips := uint32(0)
|
||||
writePendingSkips := func() {
|
||||
for pendingSkips != 0 {
|
||||
toSkip := pendingSkips
|
||||
if toSkip >= 0x1F {
|
||||
toSkip = 0x1E
|
||||
}
|
||||
writeOp(TileColorOp{Type: CtrlSkip})
|
||||
writer.Write(5, toSkip-1)
|
||||
pendingSkips -= toSkip
|
||||
}
|
||||
}
|
||||
writeLineSkip := func() {
|
||||
if pendingSkips > 0 {
|
||||
writeOp(TileColorOp{Type: CtrlSkip})
|
||||
writer.Write(5, 0x1F)
|
||||
pendingSkips = 0
|
||||
}
|
||||
}
|
||||
hasWidthConfig := seq.HTiles > 0
|
||||
for opIndex, op := range ops {
|
||||
atRowStart := hasWidthConfig && ((uint32(opIndex) % seq.HTiles) == 0)
|
||||
if (pendingSkips > 0) && atRowStart {
|
||||
writeLineSkip()
|
||||
}
|
||||
if op.Type == CtrlSkip {
|
||||
pendingSkips++
|
||||
if !hasWidthConfig {
|
||||
writePendingSkips()
|
||||
}
|
||||
} else {
|
||||
writePendingSkips()
|
||||
writeOp(op)
|
||||
}
|
||||
}
|
||||
if pendingSkips > 0 {
|
||||
writeLineSkip()
|
||||
}
|
||||
return writer.Buffer(), nil
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/compression"
|
||||
)
|
||||
|
||||
type ControlWordSequencerSuite struct {
|
||||
suite.Suite
|
||||
|
||||
sequencer compression.ControlWordSequencer
|
||||
|
||||
sequenceErr error
|
||||
sequence compression.ControlWordSequence
|
||||
hTiles uint32
|
||||
}
|
||||
|
||||
func TestControlWordSequencer(t *testing.T) {
|
||||
suite.Run(t, new(ControlWordSequencerSuite))
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) SetupTest() {
|
||||
suite.sequencer = compression.ControlWordSequencer{}
|
||||
suite.sequenceErr = nil
|
||||
suite.sequence = compression.ControlWordSequence{}
|
||||
suite.hTiles = 0
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestEmptySequence() {
|
||||
suite.whenSequenceIsCreated()
|
||||
suite.thenControlWordsShouldHaveALenOf(0)
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestAddReturnsErrorOnWrongOffset() {
|
||||
err := suite.sequencer.Add(compression.TileColorOp{Offset: compression.ControlWordParamLimit + 1})
|
||||
assert.NotNil(suite.T(), err, "error expected adding an operation with too high offset")
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestSingleOperation() {
|
||||
suite.givenRegisteredOperations(compression.TileColorOp{})
|
||||
suite.whenSequenceIsCreated()
|
||||
suite.thenControlWordsShouldHaveALenOf(1)
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestDuplicatedOperationShouldResultInOneControlWord() {
|
||||
suite.givenRegisteredOperations(compression.TileColorOp{}, compression.TileColorOp{})
|
||||
suite.whenSequenceIsCreated()
|
||||
suite.thenControlWordsShouldHaveALenOf(1)
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestControlWordsAreOrderedByOperationFrequencyFirst() {
|
||||
suite.givenRegisteredOperations(
|
||||
compression.TileColorOp{Offset: 1},
|
||||
compression.TileColorOp{}, compression.TileColorOp{})
|
||||
suite.whenSequenceIsCreated()
|
||||
suite.thenControlWordsShouldBe(compression.ControlWordOf(12, 0, 0), compression.ControlWordOf(12, 0, 1))
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestControlWordsAreOrderedByOffsetSecond() {
|
||||
suite.givenRegisteredOperations(
|
||||
compression.TileColorOp{Offset: 1}, compression.TileColorOp{Offset: 1},
|
||||
compression.TileColorOp{}, compression.TileColorOp{})
|
||||
suite.whenSequenceIsCreated()
|
||||
suite.thenControlWordsShouldBe(compression.ControlWordOf(12, 0, 0), compression.ControlWordOf(12, 0, 1))
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestControlWordsAreOrderedByControlTypeThird() {
|
||||
suite.givenRegisteredOperations(
|
||||
compression.TileColorOp{Type: compression.CtrlColorTile16ColorsMasked},
|
||||
compression.TileColorOp{Type: compression.CtrlColorTile16ColorsMasked},
|
||||
compression.TileColorOp{}, compression.TileColorOp{})
|
||||
suite.whenSequenceIsCreated()
|
||||
suite.thenControlWordsShouldBe(
|
||||
compression.ControlWordOf(12, 0, 0),
|
||||
compression.ControlWordOf(12, compression.CtrlColorTile16ColorsMasked, 0))
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestControlWordLongOffsetsWhenBitstreamSpaceExhausted_FirstTime() {
|
||||
suite.givenSequencerLimitsOf(1, 0)
|
||||
suite.givenRegisteredOperations(
|
||||
compression.TileColorOp{Offset: 0},
|
||||
compression.TileColorOp{Offset: 1},
|
||||
compression.TileColorOp{Offset: 2},
|
||||
compression.TileColorOp{Offset: 3})
|
||||
suite.whenSequenceIsCreated()
|
||||
suite.thenControlWordsShouldBe(
|
||||
compression.ControlWordOf(0, 0, 1), // first long offset jump to index 1
|
||||
compression.ControlWordOf(4, 0, 0),
|
||||
compression.ControlWordOf(4, 0, 1),
|
||||
compression.ControlWordOf(4, 0, 2),
|
||||
compression.ControlWordOf(4, 0, 3))
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestControlWordLongOffsetsWhenBitstreamSpaceExhausted_SecondTime() {
|
||||
suite.givenSequencerLimitsOf(3, 1)
|
||||
suite.givenRegisteredOperations(
|
||||
compression.TileColorOp{Offset: 0}, compression.TileColorOp{Offset: 1},
|
||||
compression.TileColorOp{Offset: 2}, compression.TileColorOp{Offset: 3},
|
||||
compression.TileColorOp{Offset: 4}, compression.TileColorOp{Offset: 5},
|
||||
compression.TileColorOp{Offset: 6}, compression.TileColorOp{Offset: 7},
|
||||
compression.TileColorOp{Offset: 8}, compression.TileColorOp{Offset: 9},
|
||||
compression.TileColorOp{Offset: 10}, compression.TileColorOp{Offset: 11},
|
||||
compression.TileColorOp{Offset: 12}, compression.TileColorOp{Offset: 13},
|
||||
compression.TileColorOp{Offset: 14}, compression.TileColorOp{Offset: 15},
|
||||
compression.TileColorOp{Offset: 16}, compression.TileColorOp{Offset: 17})
|
||||
suite.whenSequenceIsCreated()
|
||||
suite.thenControlWordsShouldBe(
|
||||
compression.ControlWordOf(12, 0, 0),
|
||||
compression.ControlWordOf(0, 0, 3), // first long offset jump to index 3
|
||||
compression.ControlWordOf(0, 0, 19), // first long offset jump to index 19
|
||||
compression.ControlWordOf(4, 0, 1), compression.ControlWordOf(4, 0, 2),
|
||||
compression.ControlWordOf(4, 0, 3), compression.ControlWordOf(4, 0, 4),
|
||||
compression.ControlWordOf(4, 0, 5), compression.ControlWordOf(4, 0, 6),
|
||||
compression.ControlWordOf(4, 0, 7), compression.ControlWordOf(4, 0, 8),
|
||||
compression.ControlWordOf(4, 0, 9), compression.ControlWordOf(4, 0, 10),
|
||||
compression.ControlWordOf(4, 0, 11), compression.ControlWordOf(4, 0, 12),
|
||||
compression.ControlWordOf(4, 0, 13), compression.ControlWordOf(4, 0, 14),
|
||||
compression.ControlWordOf(4, 0, 15), compression.ControlWordOf(4, 0, 16),
|
||||
compression.ControlWordOf(4, 0, 17))
|
||||
}
|
||||
|
||||
type bitstreamExpectation struct {
|
||||
bits int
|
||||
value uint32
|
||||
}
|
||||
|
||||
func bits12(value uint32) bitstreamExpectation {
|
||||
return bitstreamExpectation{bits: 12, value: value}
|
||||
}
|
||||
|
||||
func bits5(value uint32) bitstreamExpectation {
|
||||
return bitstreamExpectation{bits: 5, value: value}
|
||||
}
|
||||
|
||||
func bits4(value uint32) bitstreamExpectation {
|
||||
return bitstreamExpectation{bits: 4, value: value}
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestBitstreamForSimpleSequence() {
|
||||
ops := []compression.TileColorOp{{Offset: 1}, {}, {}}
|
||||
suite.givenRegisteredOperations(ops...)
|
||||
suite.whenSequenceIsCreated()
|
||||
suite.thenBitstreamShouldBeFor(ops,
|
||||
[]bitstreamExpectation{bits12(1), bits12(0), bits12(0)})
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestBitstreamForExtendedSequence() {
|
||||
suite.givenSequencerLimitsOf(2, 1)
|
||||
suite.givenRegisteredOperations(
|
||||
compression.TileColorOp{Offset: 0}, compression.TileColorOp{Offset: 1},
|
||||
compression.TileColorOp{Offset: 2}, compression.TileColorOp{Offset: 3},
|
||||
compression.TileColorOp{Offset: 4}, compression.TileColorOp{Offset: 5},
|
||||
compression.TileColorOp{Offset: 6}, compression.TileColorOp{Offset: 7},
|
||||
compression.TileColorOp{Offset: 8}, compression.TileColorOp{Offset: 9},
|
||||
compression.TileColorOp{Offset: 10}, compression.TileColorOp{Offset: 11},
|
||||
compression.TileColorOp{Offset: 12}, compression.TileColorOp{Offset: 13},
|
||||
compression.TileColorOp{Offset: 14}, compression.TileColorOp{Offset: 15},
|
||||
compression.TileColorOp{Offset: 16})
|
||||
suite.whenSequenceIsCreated()
|
||||
suite.thenBitstreamShouldBeFor([]compression.TileColorOp{{Offset: 16}},
|
||||
[]bitstreamExpectation{bits12(1), bits4(15)})
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestSkipOperation_Single() {
|
||||
suite.givenHorizontalTileCountOf(3)
|
||||
ops := []compression.TileColorOp{{Offset: 0}, {Type: compression.CtrlSkip}, {Offset: 1}}
|
||||
suite.givenRegisteredOperations(ops...)
|
||||
suite.whenSequenceIsCreated()
|
||||
suite.thenBitstreamShouldBeFor(ops,
|
||||
[]bitstreamExpectation{bits12(0), bits12(1), bits5(0), bits12(2)})
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestSkipOperation_Double() {
|
||||
suite.givenHorizontalTileCountOf(4)
|
||||
ops := []compression.TileColorOp{
|
||||
{Offset: 0}, {Type: compression.CtrlSkip},
|
||||
{Type: compression.CtrlSkip}, {Offset: 1}}
|
||||
suite.givenRegisteredOperations(ops...)
|
||||
suite.whenSequenceIsCreated()
|
||||
suite.thenBitstreamShouldBeFor(ops,
|
||||
[]bitstreamExpectation{bits12(1), bits12(0), bits5(1), bits12(2)})
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) TestSkipOperation_EndOfLine() {
|
||||
suite.givenHorizontalTileCountOf(2)
|
||||
ops := []compression.TileColorOp{
|
||||
{Offset: 0}, {Type: compression.CtrlSkip},
|
||||
{Offset: 1}, {Offset: 2}}
|
||||
suite.givenRegisteredOperations(ops...)
|
||||
suite.whenSequenceIsCreated()
|
||||
suite.thenBitstreamShouldBeFor(ops,
|
||||
[]bitstreamExpectation{bits12(0), bits12(1), bits5(0x1F), bits12(2), bits12(3)})
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) givenSequencerLimitsOf(bitstream uint32, direct uint32) {
|
||||
suite.sequencer.BitstreamIndexLimit = bitstream
|
||||
suite.sequencer.DirectIndexLimit = direct
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) givenHorizontalTileCountOf(value uint32) {
|
||||
suite.hTiles = value
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) givenRegisteredOperations(ops ...compression.TileColorOp) {
|
||||
suite.T().Helper()
|
||||
for index, op := range ops {
|
||||
err := suite.sequencer.Add(op)
|
||||
require.Nil(suite.T(), err, "no error expected adding operation %v", index)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) whenSequenceIsCreated() {
|
||||
suite.sequence, suite.sequenceErr = suite.sequencer.Sequence()
|
||||
suite.sequence.HTiles = suite.hTiles
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) thenControlWordsShouldHaveALenOf(expected int) {
|
||||
suite.T().Helper()
|
||||
require.Nil(suite.T(), suite.sequenceErr, "no error expected in this verification")
|
||||
words := suite.sequence.ControlWords()
|
||||
assert.Equal(suite.T(), expected, len(words))
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) thenControlWordsShouldBe(expected ...compression.ControlWord) {
|
||||
suite.T().Helper()
|
||||
require.Nil(suite.T(), suite.sequenceErr, "no error expected in this verification")
|
||||
words := suite.sequence.ControlWords()
|
||||
assert.Equal(suite.T(), len(expected), len(words), "length mismatch")
|
||||
assert.Equal(suite.T(), expected, words, "words mismatch")
|
||||
}
|
||||
|
||||
func (suite *ControlWordSequencerSuite) thenBitstreamShouldBeFor(ops []compression.TileColorOp, expected []bitstreamExpectation) {
|
||||
suite.T().Helper()
|
||||
require.Nil(suite.T(), suite.sequenceErr, "no error expected in this verification")
|
||||
data, err := suite.sequence.BitstreamFor(ops)
|
||||
require.Nil(suite.T(), err, "no error expected extracting bitstream")
|
||||
bitstream := compression.NewBitstreamReader(data)
|
||||
for index, exp := range expected {
|
||||
value := bitstream.Read(exp.bits)
|
||||
assert.Equal(suite.T(), exp.value, value, "Value mismatch at index %v - bitstream is 0x%v", index, hex.EncodeToString(data))
|
||||
bitstream.Advance(exp.bits)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/compression"
|
||||
)
|
||||
|
||||
func TestControlWordPackedReturnsPackedControlWord(t *testing.T) {
|
||||
word := compression.ControlWord(0x00FFAA55)
|
||||
|
||||
assert.Equal(t, compression.PackedControlWord(0x80FFAA55), word.Packed(128))
|
||||
}
|
||||
|
||||
func TestControlWordPackedClearsHighestByteBeforeSettingCount(t *testing.T) {
|
||||
word := compression.ControlWord(0x55FFAA55)
|
||||
|
||||
assert.Equal(t, compression.PackedControlWord(0xC0FFAA55), word.Packed(0xC0))
|
||||
}
|
||||
|
||||
func TestControlWordCountReturnsBits20To23(t *testing.T) {
|
||||
word := compression.ControlWord(0x00F00000)
|
||||
|
||||
assert.Equal(t, 15, word.Count())
|
||||
}
|
||||
|
||||
func TestControlWordIsLongOffsetReturnsTrueForCount0(t *testing.T) {
|
||||
word := compression.ControlWord(0x00000000)
|
||||
|
||||
assert.True(t, word.IsLongOffset())
|
||||
}
|
||||
|
||||
func TestControlWordIsLongOffsetReturnsFalseForCountGreater0(t *testing.T) {
|
||||
word := compression.ControlWord(0x00300000)
|
||||
|
||||
assert.False(t, word.IsLongOffset())
|
||||
}
|
||||
|
||||
func TestControlWordLongOffsetReturnsBits00To19(t *testing.T) {
|
||||
word := compression.ControlWord(0xFFFA6665)
|
||||
|
||||
assert.Equal(t, uint32(0xA6665), word.LongOffset())
|
||||
}
|
||||
|
||||
func TestControlWordParameterReturnsBits00To16(t *testing.T) {
|
||||
word := compression.ControlWord(0xFFFF1665)
|
||||
|
||||
assert.Equal(t, uint32(0x11665), word.Parameter())
|
||||
}
|
||||
|
||||
func TestControlWordTypeReturnsBits17To19(t *testing.T) {
|
||||
word := compression.ControlWord(0xFFFAFFFF)
|
||||
|
||||
assert.Equal(t, compression.ControlType(5), word.Type())
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package compression
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrFormat indicates errors in the compression format.
|
||||
ErrFormat ss1.StringError = "format error"
|
||||
|
||||
errCannotRepeatWordOnFirstTileOfRow ss1.StringError = "cannot repeat word on first tile of row"
|
||||
errUnknownControl ss1.StringError = "found unknown control"
|
||||
errTooManyLongOffsets ss1.StringError = "too many long offsets"
|
||||
errInvalidByteCount ss1.StringError = "invalid byte count"
|
||||
errInvalidFrameSize ss1.StringError = "invalid frame size"
|
||||
)
|
||||
|
||||
type tooManyWordsError struct {
|
||||
Total uint32
|
||||
MaximumDirect uint32
|
||||
BitstreamIndexLimit uint32
|
||||
}
|
||||
|
||||
func (err tooManyWordsError) Error() string {
|
||||
return fmt.Sprintf("too many words: total=%v, maxDirect=%v, indexLimit=%v", err.Total, err.MaximumDirect, err.BitstreamIndexLimit)
|
||||
}
|
||||
|
||||
type wordIndexOutOfRangeError struct {
|
||||
Index uint32
|
||||
Available uint32
|
||||
}
|
||||
|
||||
func (err wordIndexOutOfRangeError) Error() string {
|
||||
return fmt.Sprintf("control word index out of range: %v/%v", err.Index, err.Available)
|
||||
}
|
||||
|
||||
type paletteLookupTooBigError struct {
|
||||
Size int
|
||||
}
|
||||
|
||||
func (err paletteLookupTooBigError) Error() string {
|
||||
return fmt.Sprintf("palette lookup is too big: %vB", err.Size)
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package compression
|
||||
|
||||
// FrameDecoder is for decoding compressed frames with the help of data streams.
|
||||
// A new instance of a decoder is created with a FrameDecoderBuilder.
|
||||
type FrameDecoder struct {
|
||||
horizontalTiles int
|
||||
verticalTiles int
|
||||
|
||||
colorer TileColorFunction
|
||||
|
||||
paletteLookupList []byte
|
||||
controlWords []ControlWord
|
||||
}
|
||||
|
||||
func newFrameDecoder(builder *FrameDecoderBuilder) *FrameDecoder {
|
||||
listLen := len(builder.paletteLookupList)
|
||||
decoder := &FrameDecoder{
|
||||
horizontalTiles: builder.width / TileSideLength,
|
||||
verticalTiles: builder.height / TileSideLength,
|
||||
colorer: builder.colorer,
|
||||
paletteLookupList: make([]byte, listLen+16),
|
||||
controlWords: builder.controlWords,
|
||||
}
|
||||
|
||||
copy(decoder.paletteLookupList[:listLen], builder.paletteLookupList)
|
||||
|
||||
return decoder
|
||||
}
|
||||
|
||||
// Decode reads the provided streams to paint a frame.
|
||||
func (decoder *FrameDecoder) Decode(bitstreamData []byte, maskstreamData []byte) error {
|
||||
bitstream := NewBitstreamReader(bitstreamData)
|
||||
maskstream := NewMaskstreamReader(maskstreamData)
|
||||
|
||||
for vTile := 0; vTile < decoder.verticalTiles && !bitstream.Exhausted(); vTile++ {
|
||||
lastControl := ControlWordOf(0, CtrlUnknown, 0)
|
||||
for hTile := 0; hTile < decoder.horizontalTiles && !bitstream.Exhausted(); hTile++ {
|
||||
control, err := decoder.readNextControlWord(bitstream)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if control.Type() == CtrlRepeatPrevious {
|
||||
if hTile == 0 {
|
||||
return errCannotRepeatWordOnFirstTileOfRow
|
||||
}
|
||||
control = lastControl
|
||||
}
|
||||
if control.Type() == CtrlUnknown {
|
||||
return errUnknownControl
|
||||
}
|
||||
|
||||
if control.Type() == CtrlSkip {
|
||||
skipCount := bitstream.Read(5)
|
||||
bitstream.Advance(5)
|
||||
if skipCount == 0x1F {
|
||||
hTile = decoder.horizontalTiles
|
||||
} else {
|
||||
hTile += int(skipCount)
|
||||
}
|
||||
} else {
|
||||
decoder.colorTile(hTile, vTile, control, maskstream)
|
||||
}
|
||||
|
||||
lastControl = control
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (decoder *FrameDecoder) readNextControlWord(bitstream *BitstreamReader) (ControlWord, error) {
|
||||
availableWords := uint32(len(decoder.controlWords))
|
||||
controlIndex := bitstream.Read(12)
|
||||
if controlIndex > availableWords {
|
||||
return ControlWordOf(0, CtrlUnknown, 0),
|
||||
wordIndexOutOfRangeError{
|
||||
Index: controlIndex,
|
||||
Available: availableWords,
|
||||
}
|
||||
}
|
||||
|
||||
control := decoder.controlWords[controlIndex]
|
||||
if control.IsLongOffset() {
|
||||
longOffsetCount := 0
|
||||
bitstream.Advance(8)
|
||||
for control.IsLongOffset() {
|
||||
longOffsetCount++
|
||||
if longOffsetCount > 100 {
|
||||
return ControlWordOf(0, CtrlUnknown, 0), errTooManyLongOffsets
|
||||
}
|
||||
bitstream.Advance(4)
|
||||
offset := bitstream.Read(4)
|
||||
controlIndex = control.LongOffset() + offset
|
||||
if controlIndex > availableWords {
|
||||
return ControlWordOf(0, CtrlUnknown, 0),
|
||||
wordIndexOutOfRangeError{
|
||||
Index: controlIndex,
|
||||
Available: availableWords,
|
||||
}
|
||||
}
|
||||
control = decoder.controlWords[controlIndex]
|
||||
}
|
||||
}
|
||||
bitstream.Advance(control.Count())
|
||||
return control, nil
|
||||
}
|
||||
|
||||
func (decoder *FrameDecoder) colorTile(hTile, vTile int, control ControlWord, maskstream *MaskstreamReader) {
|
||||
param := control.Parameter()
|
||||
|
||||
switch control.Type() {
|
||||
case CtrlColorTile2ColorsStatic:
|
||||
decoder.colorer(hTile, vTile, []byte{byte(param & 0xFF), byte(param >> 8 & 0xFF)}, 0xAAAA, 1)
|
||||
case CtrlColorTile2ColorsMasked:
|
||||
decoder.colorer(hTile, vTile, []byte{byte(param & 0xFF), byte(param >> 8 & 0xFF)}, maskstream.Read(2), 1)
|
||||
case CtrlColorTile4ColorsMasked:
|
||||
decoder.colorer(hTile, vTile, decoder.paletteLookupList[param:param+4], maskstream.Read(4), 2)
|
||||
case CtrlColorTile8ColorsMasked:
|
||||
decoder.colorer(hTile, vTile, decoder.paletteLookupList[param:param+8], maskstream.Read(6), 3)
|
||||
case CtrlColorTile16ColorsMasked:
|
||||
decoder.colorer(hTile, vTile, decoder.paletteLookupList[param:param+16], maskstream.Read(8), 4)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package compression
|
||||
|
||||
// FrameDecoderBuilder implements the builder pattern for creating new instances of FrameDecoder.
|
||||
// This builder can be reused during the processing of a movie. Every Build() call creates a new instance.
|
||||
type FrameDecoderBuilder struct {
|
||||
width int
|
||||
height int
|
||||
|
||||
colorer TileColorFunction
|
||||
|
||||
paletteLookupList []byte
|
||||
controlWords []ControlWord
|
||||
}
|
||||
|
||||
// NewFrameDecoderBuilder returns a new instance of a builder with given initial values.
|
||||
func NewFrameDecoderBuilder(width, height int) *FrameDecoderBuilder {
|
||||
return &FrameDecoderBuilder{
|
||||
width: width,
|
||||
height: height,
|
||||
}
|
||||
}
|
||||
|
||||
// Build creates a new instance of a decoder with the most recent parameters.
|
||||
func (builder *FrameDecoderBuilder) Build() *FrameDecoder {
|
||||
return newFrameDecoder(builder)
|
||||
}
|
||||
|
||||
// ForStandardFrame registers the frame buffer and sets the standard coloring method.
|
||||
func (builder *FrameDecoderBuilder) ForStandardFrame(frame []byte, stride int) {
|
||||
builder.colorer = StandardTileColorer(frame, stride)
|
||||
}
|
||||
|
||||
// WithPaletteLookupList registers the new list.
|
||||
func (builder *FrameDecoderBuilder) WithPaletteLookupList(list []byte) {
|
||||
builder.paletteLookupList = list
|
||||
}
|
||||
|
||||
// WithControlWords registers the new word dictionary.
|
||||
func (builder *FrameDecoderBuilder) WithControlWords(words []ControlWord) {
|
||||
builder.controlWords = words
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package compression
|
||||
|
||||
// MaskstreamWriter writes mask integers to a byte array.
|
||||
type MaskstreamWriter struct {
|
||||
Buffer []byte
|
||||
}
|
||||
|
||||
// Write adds the given amount of bytes from the mask to the buffer.
|
||||
func (w *MaskstreamWriter) Write(byteCount uint, mask uint64) error {
|
||||
if byteCount > 8 {
|
||||
return errInvalidByteCount
|
||||
}
|
||||
if w.Buffer == nil {
|
||||
w.Buffer = make([]byte, 0, 1024*32)
|
||||
}
|
||||
for b := uint(0); b < byteCount; b++ {
|
||||
w.Buffer = append(w.Buffer, byte(mask>>(b*8)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MaskstreamReader reads mask integers from a byte array.
|
||||
type MaskstreamReader struct {
|
||||
source []byte
|
||||
curPos int
|
||||
}
|
||||
|
||||
// NewMaskstreamReader returns a new reader instance for given source.
|
||||
func NewMaskstreamReader(source []byte) *MaskstreamReader {
|
||||
return &MaskstreamReader{source: source}
|
||||
}
|
||||
|
||||
// Read returns a mask integer of given byte length from the current position.
|
||||
// If the current position is at or beyond the available size, 0x00 is assumed for the missing bytes.
|
||||
//
|
||||
// Reading more than 8, or less than 0, bytes panics.
|
||||
func (reader *MaskstreamReader) Read(bytes int) (value uint64) {
|
||||
if bytes > 8 {
|
||||
panic("Limit of byte count: 8")
|
||||
}
|
||||
if bytes < 0 {
|
||||
panic("Minimum byte count: 0")
|
||||
}
|
||||
|
||||
for i := 0; i < bytes; i++ {
|
||||
value |= reader.nextByte() << uint64(8*i)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (reader *MaskstreamReader) nextByte() (value uint64) {
|
||||
if reader.curPos < len(reader.source) {
|
||||
value = uint64(reader.source[reader.curPos])
|
||||
reader.curPos++
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/compression"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMaskstreamReaderReadPanicsForMoreThan8Bytes(t *testing.T) {
|
||||
reader := compression.NewMaskstreamReader([]byte{})
|
||||
|
||||
assert.Panicsf(t, func() { reader.Read(9) }, "Limit of byte count: 8")
|
||||
}
|
||||
|
||||
func TestMaskstreamReaderReadPanicsForLessThan0Bytes(t *testing.T) {
|
||||
reader := compression.NewMaskstreamReader([]byte{})
|
||||
|
||||
assert.Panicsf(t, func() { reader.Read(-1) }, "Minimum byte count: 0")
|
||||
}
|
||||
|
||||
func TestMaskstreamReaderReadReturnsValueOfRequestedByteSize(t *testing.T) {
|
||||
reader := compression.NewMaskstreamReader([]byte{0xAF})
|
||||
|
||||
result := reader.Read(1)
|
||||
|
||||
assert.Equal(t, uint64(0xAF), result)
|
||||
}
|
||||
|
||||
func TestMaskstreamReaderReadIntegerFromSourceInLittleEndianOrder(t *testing.T) {
|
||||
reader := compression.NewMaskstreamReader([]byte{0x11, 0x22})
|
||||
|
||||
result := reader.Read(2)
|
||||
|
||||
assert.Equal(t, uint64(0x2211), result)
|
||||
}
|
||||
|
||||
func TestMaskstreamReaderReadCanProvideUpTo64BitValues(t *testing.T) {
|
||||
reader := compression.NewMaskstreamReader([]byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88})
|
||||
|
||||
result := reader.Read(8)
|
||||
|
||||
assert.Equal(t, uint64(0x8877665544332211), result)
|
||||
}
|
||||
|
||||
func TestMaskstreamReaderReadFillsMissingBytesWithZero(t *testing.T) {
|
||||
reader := compression.NewMaskstreamReader([]byte{0xAA, 0xBB})
|
||||
|
||||
result := reader.Read(8)
|
||||
|
||||
assert.Equal(t, uint64(0x00BBAA), result)
|
||||
}
|
||||
|
||||
func TestMaskstreamReaderReadAdvancesCurrentPosition(t *testing.T) {
|
||||
reader := compression.NewMaskstreamReader([]byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88})
|
||||
|
||||
reader.Read(2)
|
||||
result := reader.Read(3)
|
||||
|
||||
assert.Equal(t, uint64(0x554433), result)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package compression
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// PackControlWords packs and encodes the given words into a byte stream.
|
||||
func PackControlWords(words []ControlWord) []byte {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
controlBytes := uint32(len(words) * bytesPerControlWord)
|
||||
|
||||
_ = binary.Write(buf, binary.LittleEndian, controlBytes)
|
||||
if controlBytes > 0 {
|
||||
lastPacked := words[0].Packed(1)
|
||||
|
||||
for _, word := range words[1:] {
|
||||
if (lastPacked.Value() == word) && (lastPacked.Times() < 0xFF) {
|
||||
lastPacked = word.Packed(lastPacked.Times() + 1)
|
||||
} else {
|
||||
_ = binary.Write(buf, binary.LittleEndian, uint32(lastPacked))
|
||||
lastPacked = word.Packed(1)
|
||||
}
|
||||
}
|
||||
_ = binary.Write(buf, binary.LittleEndian, uint32(lastPacked))
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/compression"
|
||||
)
|
||||
|
||||
func TestControlWordsEmptyArrayResultsInZeroCount(t *testing.T) {
|
||||
data := compression.PackControlWords(nil)
|
||||
|
||||
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, data)
|
||||
}
|
||||
|
||||
func TestControlWordsCountIsInLengthTimesThree(t *testing.T) {
|
||||
data := compression.PackControlWords([]compression.ControlWord{
|
||||
compression.ControlWord(0),
|
||||
compression.ControlWord(1),
|
||||
})
|
||||
|
||||
assert.Equal(t, []byte{0x06, 0x00, 0x00, 0x00}, data[:4])
|
||||
}
|
||||
|
||||
func TestControlWordsSingleItem(t *testing.T) {
|
||||
data := compression.PackControlWords([]compression.ControlWord{compression.ControlWord(0x00BBCCDD)})
|
||||
|
||||
assert.Equal(t, []byte{0x03, 0x00, 0x00, 0x00, 0xDD, 0xCC, 0xBB, 0x01}, data)
|
||||
}
|
||||
|
||||
func TestControlWordsMultiItemArray(t *testing.T) {
|
||||
data := compression.PackControlWords([]compression.ControlWord{
|
||||
compression.ControlWord(0x00BBCCDD),
|
||||
compression.ControlWord(0x00112233),
|
||||
})
|
||||
|
||||
assert.Equal(t, []byte{0x06, 0x00, 0x00, 0x00, 0xDD, 0xCC, 0xBB, 0x01, 0x33, 0x22, 0x11, 0x01}, data)
|
||||
}
|
||||
|
||||
func TestControlWordsIdenticalWordsArePacked(t *testing.T) {
|
||||
data := compression.PackControlWords([]compression.ControlWord{
|
||||
compression.ControlWord(0x00BBCCDD),
|
||||
compression.ControlWord(0x00BBCCDD),
|
||||
})
|
||||
|
||||
require.Equal(t, 8, len(data))
|
||||
assert.Equal(t, []byte{0xDD, 0xCC, 0xBB, 0x02}, data[4:8])
|
||||
}
|
||||
|
||||
func TestControlWordsCountIsResetForFurtherWords(t *testing.T) {
|
||||
data := compression.PackControlWords([]compression.ControlWord{
|
||||
compression.ControlWord(0x00BBCCDD),
|
||||
compression.ControlWord(0x00BBCCDD),
|
||||
compression.ControlWord(0x00112233),
|
||||
})
|
||||
|
||||
require.Equal(t, 12, len(data))
|
||||
assert.Equal(t, []byte{0xDD, 0xCC, 0xBB, 0x02, 0x33, 0x22, 0x11, 0x01}, data[4:12])
|
||||
}
|
||||
|
||||
func TestControlWordsMaximumCountIs255(t *testing.T) {
|
||||
words := make([]compression.ControlWord, 260)
|
||||
for i := 0; i < len(words); i++ {
|
||||
words[i] = compression.ControlWord(0x00112233)
|
||||
}
|
||||
data := compression.PackControlWords(words)
|
||||
|
||||
require.Equal(t, 12, len(data))
|
||||
assert.Equal(t, []byte{0x0C, 0x03, 0x00, 0x00, 0x33, 0x22, 0x11, 0xFF, 0x33, 0x22, 0x11, 0x05}, data)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package compression
|
||||
|
||||
// PackedControlWord contains a ControlWord together with a count value of occurrences.
|
||||
type PackedControlWord uint32
|
||||
|
||||
// Times returns the amount of occurrences of the contained control word.
|
||||
func (packed PackedControlWord) Times() int {
|
||||
return int((packed >> 24) & 0xFF)
|
||||
}
|
||||
|
||||
// Value returns the control word value.
|
||||
func (packed PackedControlWord) Value() ControlWord {
|
||||
return ControlWord(uint32(packed) & 0x00FFFFFF)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/compression"
|
||||
)
|
||||
|
||||
func TestPackedControlWordTimesReturnsHighestByte(t *testing.T) {
|
||||
packed := compression.PackedControlWord(0xFF706050)
|
||||
|
||||
assert.Equal(t, int(0xFF), packed.Times())
|
||||
}
|
||||
|
||||
func TestPackedControlWordValueReturnsTheControlWord(t *testing.T) {
|
||||
packed := compression.PackedControlWord(0xFF706050)
|
||||
|
||||
assert.Equal(t, compression.ControlWord(0x00706050), packed.Value())
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package compression
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/bits"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type paletteLookupEntry struct {
|
||||
start int
|
||||
size int
|
||||
}
|
||||
|
||||
// PaletteLookup is a dictionary of tile delta data, found in a palette buffer.
|
||||
type PaletteLookup struct {
|
||||
buffer []byte
|
||||
entries map[TilePaletteKey]paletteLookupEntry
|
||||
}
|
||||
|
||||
// Buffer returns the underlying slice.
|
||||
func (lookup *PaletteLookup) Buffer() []byte {
|
||||
return lookup.buffer
|
||||
}
|
||||
|
||||
// Lookup finds the given tile again and returns the properties where and how to reproduce it.
|
||||
func (lookup *PaletteLookup) Lookup(tile tileDelta) (index int, pal []byte, mask uint64) {
|
||||
key := TilePaletteKeyFrom(tile[:])
|
||||
entry, inLookup := lookup.entries[key]
|
||||
if inLookup {
|
||||
index = entry.start
|
||||
pal = lookup.buffer[entry.start : entry.start+entry.size]
|
||||
} else {
|
||||
pal = key.Buffer()
|
||||
}
|
||||
var mapped [256]byte
|
||||
for mappedIndex := len(pal) - 1; mappedIndex >= 0; mappedIndex-- {
|
||||
mapped[pal[mappedIndex]] = byte(mappedIndex)
|
||||
}
|
||||
bitSize := uint(bits.Len(uint(len(pal) - 1)))
|
||||
for tileIndex := PixelPerTile - 1; tileIndex >= 0; tileIndex-- {
|
||||
mask <<= bitSize
|
||||
mask |= uint64(mapped[tile[tileIndex]])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// PaletteLookupGenerator creates palette lookups based on a set of registered tiles.
|
||||
type PaletteLookupGenerator struct {
|
||||
keyUses map[TilePaletteKey]int
|
||||
}
|
||||
|
||||
// Generate creates a lookup based on all currently registered tile deltas.
|
||||
func (gen *PaletteLookupGenerator) Generate(ctx context.Context) (PaletteLookup, error) {
|
||||
var lookup PaletteLookup
|
||||
lookup.entries = make(map[TilePaletteKey]paletteLookupEntry)
|
||||
|
||||
remainder := make(map[TilePaletteKey]struct{})
|
||||
for key := range gen.keyUses {
|
||||
remainder[key] = struct{}{}
|
||||
}
|
||||
|
||||
type sizedEntry struct {
|
||||
entries map[TilePaletteKey]paletteLookupEntry
|
||||
lastOffset int
|
||||
}
|
||||
sizedEntries := make(map[int]*sizedEntry)
|
||||
knownSizes := []int{4, 8, 16}
|
||||
for _, size := range knownSizes {
|
||||
sizedEntries[size] = &sizedEntry{
|
||||
entries: make(map[TilePaletteKey]paletteLookupEntry),
|
||||
}
|
||||
}
|
||||
|
||||
toDeleteCache := make([]TilePaletteKey, 0, len(remainder))
|
||||
addToBuffer := func(data []byte) {
|
||||
lookup.buffer = append(lookup.buffer, data...)
|
||||
|
||||
newSize := len(lookup.buffer)
|
||||
|
||||
for _, fitSize := range knownSizes {
|
||||
fitLimit := newSize - fitSize
|
||||
entry := sizedEntries[fitSize]
|
||||
|
||||
// remove all entries beyond a certain limit. as these bytes don't change, retrying won't help.
|
||||
toDelete := toDeleteCache[0:0]
|
||||
limit := newSize - 16 - len(data)
|
||||
for key, entry := range entry.entries {
|
||||
if entry.start < limit {
|
||||
toDelete = append(toDelete, key)
|
||||
}
|
||||
}
|
||||
for _, key := range toDelete {
|
||||
delete(entry.entries, key)
|
||||
}
|
||||
|
||||
// find any new keys
|
||||
for start := entry.lastOffset; start < fitLimit; start++ {
|
||||
tempKey := TilePaletteKeyFrom(lookup.buffer[start : start+fitSize])
|
||||
if _, existing := entry.entries[tempKey]; !existing {
|
||||
entry.entries[tempKey] = paletteLookupEntry{
|
||||
start: start,
|
||||
size: fitSize,
|
||||
}
|
||||
}
|
||||
}
|
||||
if fitLimit > 0 {
|
||||
entry.lastOffset = fitLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addEarlyEntry := func(key TilePaletteKey, limitSize int) bool {
|
||||
for _, fitSize := range knownSizes {
|
||||
if key.size <= fitSize && fitSize <= limitSize {
|
||||
entry := sizedEntries[fitSize]
|
||||
for tempKey, paletteEntry := range entry.entries {
|
||||
if tempKey.Contains(&key) && (!key.HasColor(0x00) || (lookup.buffer[paletteEntry.start] == 0x00)) {
|
||||
lookup.entries[key] = paletteEntry
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
sizeLimitForSize := map[int]int{3: 4, 4: 8, 5: 8, 6: 8, 7: 8, 8: 8, 9: 16, 10: 16, 11: 16, 12: 16, 13: 16, 14: 16, 15: 16, 16: 16}
|
||||
for size := PixelPerTile; (size > 2) && (ctx.Err() == nil); size-- {
|
||||
keysInSize := make([]TilePaletteKey, 0, len(remainder))
|
||||
for key := range remainder {
|
||||
if key.size == size {
|
||||
keysInSize = append(keysInSize, key)
|
||||
}
|
||||
}
|
||||
sort.Slice(keysInSize, func(a, b int) bool { return keysInSize[a].LessThan(&keysInSize[b]) })
|
||||
|
||||
// fmt.Printf("Working on key size %v, have %v sized, %v total remaining\n", size, len(keysInSize), len(remainder))
|
||||
for _, sizedKey := range keysInSize {
|
||||
if ctx.Err() != nil {
|
||||
return PaletteLookup{}, ctx.Err()
|
||||
}
|
||||
|
||||
{
|
||||
var earlyRemoved []TilePaletteKey
|
||||
for key := range remainder {
|
||||
if addEarlyEntry(key, sizeLimitForSize[size]) {
|
||||
earlyRemoved = append(earlyRemoved, key)
|
||||
}
|
||||
}
|
||||
for _, key := range earlyRemoved {
|
||||
delete(remainder, key)
|
||||
}
|
||||
}
|
||||
if _, stillRemaining := remainder[sizedKey]; stillRemaining {
|
||||
bytes := sizedKey.Buffer()
|
||||
lookup.entries[sizedKey] = paletteLookupEntry{start: len(lookup.buffer), size: len(bytes)}
|
||||
addToBuffer(bytes)
|
||||
|
||||
delete(remainder, sizedKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lookup, ctx.Err()
|
||||
}
|
||||
|
||||
// Add registers a further delta to the generator.
|
||||
func (gen *PaletteLookupGenerator) Add(delta tileDelta) {
|
||||
key := TilePaletteKeyFrom(delta[:])
|
||||
if key.size > 2 {
|
||||
if gen.keyUses == nil {
|
||||
gen.keyUses = make(map[TilePaletteKey]int)
|
||||
}
|
||||
gen.keyUses[key]++
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package compression
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// EncodedFrame contains the streams of one compressed frame.
|
||||
type EncodedFrame struct {
|
||||
Bitstream []byte
|
||||
Maskstream []byte
|
||||
}
|
||||
|
||||
type tileDelta [PixelPerTile]byte
|
||||
|
||||
type frameDelta struct {
|
||||
tiles []tileDelta
|
||||
}
|
||||
|
||||
// SceneEncoder encodes an entire scene of bitmaps sharing the same palette.
|
||||
type SceneEncoder struct {
|
||||
hTiles int
|
||||
vTiles int
|
||||
lineStride int
|
||||
tileStride int
|
||||
|
||||
lastFrame []byte
|
||||
deltas []frameDelta
|
||||
}
|
||||
|
||||
// NewSceneEncoder returns a new instance.
|
||||
func NewSceneEncoder(width, height int) *SceneEncoder {
|
||||
e := &SceneEncoder{
|
||||
hTiles: width / TileSideLength,
|
||||
vTiles: height / TileSideLength,
|
||||
lineStride: width,
|
||||
}
|
||||
e.tileStride = e.lineStride * TileSideLength
|
||||
e.lastFrame = make([]byte, e.vTiles*TileSideLength*e.lineStride)
|
||||
return e
|
||||
}
|
||||
|
||||
// AddFrame registers a further frame to the scene.
|
||||
func (e *SceneEncoder) AddFrame(frame []byte) error {
|
||||
if len(frame) != len(e.lastFrame) {
|
||||
return errInvalidFrameSize
|
||||
}
|
||||
var delta frameDelta
|
||||
isFirstFrame := len(e.deltas) == 0
|
||||
vStart := 0
|
||||
for vTile := 0; vTile < e.vTiles; vTile++ {
|
||||
tileStart := vStart
|
||||
for hTile := 0; hTile < e.hTiles; hTile++ {
|
||||
delta.tiles = append(delta.tiles, e.deltaTile(isFirstFrame, tileStart, frame))
|
||||
tileStart += TileSideLength
|
||||
}
|
||||
vStart += e.tileStride
|
||||
}
|
||||
e.deltas = append(e.deltas, delta)
|
||||
copy(e.lastFrame, frame)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *SceneEncoder) deltaTile(isFirstFrame bool, offset int, frame []byte) tileDelta {
|
||||
var delta tileDelta
|
||||
for y := 0; y < TileSideLength; y++ {
|
||||
start := offset + (y * e.lineStride)
|
||||
for x := 0; x < TileSideLength; x++ {
|
||||
pixel := frame[start+x]
|
||||
if isFirstFrame || (pixel != e.lastFrame[start+x]) {
|
||||
delta[y*TileSideLength+x] = pixel
|
||||
}
|
||||
}
|
||||
}
|
||||
return delta
|
||||
}
|
||||
|
||||
// Encode processes all the previously registered frames and creates the necessary components for decoding.
|
||||
func (e *SceneEncoder) Encode(ctx context.Context) (
|
||||
words []ControlWord, paletteLookupBuffer []byte, frames []EncodedFrame, err error) {
|
||||
var wordSequencer ControlWordSequencer
|
||||
tileColorOpsPerFrame := make([][]TileColorOp, len(e.deltas))
|
||||
paletteLookup, err := e.createPaletteLookup(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
paletteLookupBuffer = paletteLookup.Buffer()
|
||||
if len(paletteLookupBuffer) > 0x1FFFF {
|
||||
err = paletteLookupTooBigError{Size: len(paletteLookupBuffer)}
|
||||
return
|
||||
}
|
||||
|
||||
frames = make([]EncodedFrame, len(e.deltas))
|
||||
for frameIndex := 0; frameIndex < len(e.deltas); frameIndex++ {
|
||||
var maskstreamWriter MaskstreamWriter
|
||||
outFrame := &frames[frameIndex]
|
||||
delta := e.deltas[frameIndex]
|
||||
|
||||
lastOp := TileColorOp{Type: CtrlUnknown}
|
||||
for tileIndex, tile := range delta.tiles {
|
||||
var op TileColorOp
|
||||
paletteIndex, pal, mask := paletteLookup.Lookup(tile)
|
||||
palSize := len(pal)
|
||||
|
||||
switch {
|
||||
case palSize == 1 && (pal[0] == 0x00):
|
||||
op.Type = CtrlSkip
|
||||
case palSize == 1:
|
||||
op.Type = CtrlColorTile2ColorsStatic
|
||||
op.Offset = uint32(pal[0])<<8 | uint32(pal[0])
|
||||
case palSize == 2 && mask == 0xAAAA && (pal[0] != 0x00) && (pal[1] != 0x00):
|
||||
op.Type = CtrlColorTile2ColorsStatic
|
||||
op.Offset = uint32(pal[1])<<8 | uint32(pal[0])
|
||||
case palSize == 2 && mask == 0x5555 && (pal[0] != 0x00) && (pal[1] != 0x00):
|
||||
op.Type = CtrlColorTile2ColorsStatic
|
||||
op.Offset = uint32(pal[0])<<8 | uint32(pal[1])
|
||||
case palSize <= 2:
|
||||
op.Type = CtrlColorTile2ColorsMasked
|
||||
if palSize == 2 {
|
||||
op.Offset = uint32(pal[1])
|
||||
op.Offset <<= 8
|
||||
}
|
||||
if palSize > 0 {
|
||||
op.Offset |= uint32(pal[0])
|
||||
}
|
||||
|
||||
_ = maskstreamWriter.Write(2, mask)
|
||||
case palSize <= 4:
|
||||
op.Type = CtrlColorTile4ColorsMasked
|
||||
op.Offset = uint32(paletteIndex)
|
||||
_ = maskstreamWriter.Write(4, mask)
|
||||
case palSize <= 8:
|
||||
op.Type = CtrlColorTile8ColorsMasked
|
||||
op.Offset = uint32(paletteIndex)
|
||||
_ = maskstreamWriter.Write(6, mask)
|
||||
default:
|
||||
op.Type = CtrlColorTile16ColorsMasked
|
||||
op.Offset = uint32(paletteIndex)
|
||||
_ = maskstreamWriter.Write(8, mask)
|
||||
}
|
||||
|
||||
if op.Type != CtrlSkip && (tileIndex%e.hTiles) != 0 && lastOp == op {
|
||||
op = TileColorOp{Type: CtrlRepeatPrevious}
|
||||
} else {
|
||||
lastOp = op
|
||||
}
|
||||
|
||||
err = wordSequencer.Add(op)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
tileColorOpsPerFrame[frameIndex] = append(tileColorOpsPerFrame[frameIndex], op)
|
||||
}
|
||||
outFrame.Maskstream = maskstreamWriter.Buffer
|
||||
}
|
||||
|
||||
wordSequence, err := wordSequencer.Sequence()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
wordSequence.HTiles = uint32(e.hTiles)
|
||||
words = wordSequence.ControlWords()
|
||||
for frameIndex, ops := range tileColorOpsPerFrame {
|
||||
frames[frameIndex].Bitstream, err = wordSequence.BitstreamFor(ops)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (e *SceneEncoder) createPaletteLookup(ctx context.Context) (PaletteLookup, error) {
|
||||
var paletteLookupGenerator PaletteLookupGenerator
|
||||
for _, delta := range e.deltas {
|
||||
for _, tile := range delta.tiles {
|
||||
paletteLookupGenerator.Add(tile)
|
||||
}
|
||||
}
|
||||
return paletteLookupGenerator.Generate(ctx)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package compression
|
||||
|
||||
// StandardTileColorer returns a TileColorFunction for given frame buffer.
|
||||
//
|
||||
// This colorer iterates over all pixel of a tile. It sets the pixel value if the
|
||||
// corresponding palette value is > 0x00.
|
||||
func StandardTileColorer(frame []byte, stride int) TileColorFunction {
|
||||
return func(hTile int, vTile int, lookupArray []byte, mask uint64, indexBitSize uint64) {
|
||||
start := vTile*TileSideLength*stride + hTile*TileSideLength
|
||||
singleMask := ^(^uint64(0) << indexBitSize)
|
||||
|
||||
for i := 0; i < PixelPerTile; i++ {
|
||||
pixelValue := lookupArray[(mask>>(indexBitSize*uint64(i)))&singleMask]
|
||||
|
||||
if pixelValue != 0x00 {
|
||||
offset := start + (i % TileSideLength) + stride*(i/TileSideLength)
|
||||
|
||||
frame[offset] = pixelValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/compression"
|
||||
)
|
||||
|
||||
type StandardTileColorerSuite struct {
|
||||
suite.Suite
|
||||
|
||||
buffer []byte
|
||||
stride int
|
||||
|
||||
colorer compression.TileColorFunction
|
||||
}
|
||||
|
||||
func TestStandardTileColorerSuite(t *testing.T) {
|
||||
suite.Run(t, new(StandardTileColorerSuite))
|
||||
}
|
||||
|
||||
func (suite *StandardTileColorerSuite) SetupTest() {
|
||||
suite.buffer = make([]byte, compression.PixelPerTile*9)
|
||||
suite.stride = compression.TileSideLength * 3
|
||||
|
||||
for i := 0; i < len(suite.buffer); i++ {
|
||||
suite.buffer[i] = 0xDD
|
||||
}
|
||||
|
||||
suite.colorer = compression.StandardTileColorer(suite.buffer, suite.stride)
|
||||
}
|
||||
|
||||
func (suite *StandardTileColorerSuite) TestFunctionColorsAllPixelOfATile() {
|
||||
suite.whenTileIsColored(1, 1, []byte{0xFF, 0x00}, 0, 1)
|
||||
|
||||
suite.thenTileShouldBe(1, 1, []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF})
|
||||
}
|
||||
|
||||
func (suite *StandardTileColorerSuite) TestColoringATileLeavesPixelOfOthersAlone() {
|
||||
suite.whenTileIsColored(1, 1, []byte{0xFF}, 0, 1)
|
||||
|
||||
suite.thenTileShouldBeUntouched(0, 0)
|
||||
suite.thenTileShouldBeUntouched(1, 0)
|
||||
suite.thenTileShouldBeUntouched(2, 0)
|
||||
suite.thenTileShouldBeUntouched(0, 1)
|
||||
suite.thenTileShouldBeUntouched(2, 1)
|
||||
suite.thenTileShouldBeUntouched(0, 2)
|
||||
suite.thenTileShouldBeUntouched(1, 2)
|
||||
suite.thenTileShouldBeUntouched(2, 2)
|
||||
}
|
||||
|
||||
func (suite *StandardTileColorerSuite) TestFunctionWorksWithMaximumIndexSize() {
|
||||
suite.whenTileIsColored(1, 1, []byte{0x01, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
|
||||
0xFEDCBA9876543210, 4)
|
||||
|
||||
suite.thenTileShouldBe(1, 1, []byte{0x01, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF})
|
||||
}
|
||||
|
||||
func (suite *StandardTileColorerSuite) TestFunctionSkipsZeroPaletteIndices() {
|
||||
suite.givenTileIsFilled(1, 1, 0xFF)
|
||||
|
||||
suite.whenTileIsColored(1, 1, []byte{0x33, 0x00}, 0xAAAA, 1)
|
||||
|
||||
suite.thenTileShouldBe(1, 1, []byte{0x33, 0xFF, 0x33, 0xFF, 0x33, 0xFF, 0x33, 0xFF, 0x33, 0xFF, 0x33, 0xFF, 0x33, 0xFF, 0x33, 0xFF})
|
||||
}
|
||||
|
||||
func (suite *StandardTileColorerSuite) givenTileIsFilled(hTile int, vTile int, initValue byte) {
|
||||
suite.colorer(hTile, vTile, []byte{initValue, 0xEE}, 0x0000, 1)
|
||||
}
|
||||
|
||||
func (suite *StandardTileColorerSuite) whenTileIsColored(hTile int, vTile int, lookupArray []byte, mask uint64, indexBitSize uint64) {
|
||||
suite.colorer(hTile, vTile, lookupArray, mask, indexBitSize)
|
||||
}
|
||||
|
||||
func (suite *StandardTileColorerSuite) thenTileShouldBe(hTile, vTile int, expected []byte) {
|
||||
tile := suite.getTile(hTile, vTile)
|
||||
|
||||
assert.Equal(suite.T(), expected, tile)
|
||||
}
|
||||
|
||||
func (suite *StandardTileColorerSuite) thenTileShouldBeUntouched(hTile, vTile int) {
|
||||
expected := make([]byte, compression.PixelPerTile)
|
||||
for i := 0; i < len(expected); i++ {
|
||||
expected[i] = 0xDD
|
||||
}
|
||||
|
||||
suite.thenTileShouldBe(hTile, vTile, expected)
|
||||
}
|
||||
|
||||
func (suite *StandardTileColorerSuite) getTile(hTile, vTile int) []byte {
|
||||
tileBuffer := make([]byte, compression.PixelPerTile)
|
||||
start := vTile*compression.TileSideLength*suite.stride + hTile*compression.TileSideLength
|
||||
|
||||
for i := 0; i < compression.TileSideLength; i++ {
|
||||
outOffset := compression.TileSideLength * i
|
||||
inOffset := start + suite.stride*i
|
||||
copy(tileBuffer[outOffset:outOffset+compression.TileSideLength], suite.buffer[inOffset:inOffset+compression.TileSideLength])
|
||||
}
|
||||
|
||||
return tileBuffer
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package compression
|
||||
|
||||
// TileColorFunction is one which shall color the pixel of a video frame tile using the provided values.
|
||||
// mask is a packed field of 16 indices, each with a length of indexBitSize. These indices point into a
|
||||
// value of lookupArray.
|
||||
type TileColorFunction func(hTile int, vTile int, lookupArray []byte, mask uint64, indexBitSize uint64)
|
||||
@@ -0,0 +1,70 @@
|
||||
package compression
|
||||
|
||||
import "math/bits"
|
||||
|
||||
// TilePaletteKey marks use of colors of a 256 palette.
|
||||
type TilePaletteKey struct {
|
||||
usedColors [4]uint64
|
||||
size int
|
||||
}
|
||||
|
||||
// TilePaletteKeyFrom returns a key for the given slice of colors.
|
||||
func TilePaletteKeyFrom(colors []byte) TilePaletteKey {
|
||||
var key TilePaletteKey
|
||||
for _, c := range colors {
|
||||
key.UseColor(c)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// Buffer returns the color indices marked in use by this key.
|
||||
func (key *TilePaletteKey) Buffer() []byte {
|
||||
result := make([]byte, 0, key.size)
|
||||
for i := 0; i < 256; i++ {
|
||||
if key.HasColor(byte(i)) {
|
||||
result = append(result, byte(i))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// UseColor marks the given color index in use.
|
||||
func (key *TilePaletteKey) UseColor(index byte) {
|
||||
if !key.HasColor(index) {
|
||||
key.usedColors[index/64] |= 1 << uint(index%64)
|
||||
key.size++
|
||||
}
|
||||
}
|
||||
|
||||
// HasColor returns true if the given color index is in use by this key.
|
||||
func (key *TilePaletteKey) HasColor(index byte) bool {
|
||||
return (key.usedColors[index/64] & (1 << uint(index%64))) != 0
|
||||
}
|
||||
|
||||
// Contains returns true if this key is equal to, or a superset of, the given key.
|
||||
func (key *TilePaletteKey) Contains(other *TilePaletteKey) bool {
|
||||
return ((^key.usedColors[0] & other.usedColors[0]) == 0) &&
|
||||
((^key.usedColors[1] & other.usedColors[1]) == 0) &&
|
||||
((^key.usedColors[2] & other.usedColors[2]) == 0) &&
|
||||
((^key.usedColors[3] & other.usedColors[3]) == 0)
|
||||
}
|
||||
|
||||
// Without returns a new key instance that has all remaining colors marked in use.
|
||||
func (key *TilePaletteKey) Without(other *TilePaletteKey) TilePaletteKey {
|
||||
var result TilePaletteKey
|
||||
for i := 0; i < 4; i++ {
|
||||
result.usedColors[i] = key.usedColors[i] & ^other.usedColors[i]
|
||||
result.size += bits.OnesCount64(result.usedColors[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// LessThan is a sorting function to order keys.
|
||||
func (key *TilePaletteKey) LessThan(other *TilePaletteKey) bool {
|
||||
for i := 0; i < 4; i++ {
|
||||
if key.usedColors[i] < other.usedColors[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/compression"
|
||||
)
|
||||
|
||||
func TestTilePaletteKeyNullValueHasNoColorsSet(t *testing.T) {
|
||||
var key compression.TilePaletteKey
|
||||
for color := 0; color < 256; color++ {
|
||||
assert.False(t, key.HasColor(byte(color)), "color set although should not %v", color)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTilePaletteKeyCanMarkSingleColors(t *testing.T) {
|
||||
for color := 0; color < 256; color++ {
|
||||
var key compression.TilePaletteKey
|
||||
key.UseColor(byte(color))
|
||||
assert.True(t, key.HasColor(byte(color)), "color not set %v", color)
|
||||
for otherColor := 0; otherColor < 256; otherColor++ {
|
||||
if otherColor != color {
|
||||
assert.False(t, key.HasColor(byte(otherColor)),
|
||||
"color %v set although should not for %v", otherColor, color)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTilePaletteKeyCanMarkAllColors(t *testing.T) {
|
||||
var key compression.TilePaletteKey
|
||||
for color := 0; color < 256; color++ {
|
||||
key.UseColor(byte(color))
|
||||
}
|
||||
for color := 0; color < 256; color++ {
|
||||
assert.True(t, key.HasColor(byte(color)), "should have color set %v", color)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTilePaletteKeyContains(t *testing.T) {
|
||||
baseKey := compression.TilePaletteKeyFrom([]byte{1, 2, 3, 4})
|
||||
tt := []struct {
|
||||
other []byte
|
||||
expected bool
|
||||
}{
|
||||
{other: []byte{1, 2, 3, 4}, expected: true},
|
||||
{other: []byte{2, 3, 4}, expected: true},
|
||||
{other: []byte{1, 2, 4}, expected: true},
|
||||
{other: []byte{4}, expected: true},
|
||||
{other: []byte{}, expected: true},
|
||||
{other: []byte{1, 2, 3, 4, 5}, expected: false},
|
||||
{other: []byte{3, 4, 5}, expected: false},
|
||||
{other: []byte{5}, expected: false},
|
||||
}
|
||||
|
||||
for index, tc := range tt {
|
||||
td := tc
|
||||
t.Run(fmt.Sprintf("%v", index), func(t *testing.T) {
|
||||
other := compression.TilePaletteKeyFrom(td.other)
|
||||
result := baseKey.Contains(&other)
|
||||
assert.Equal(t, result, td.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTilePaletteKeyWithout(t *testing.T) {
|
||||
baseKey := compression.TilePaletteKeyFrom([]byte{1, 2, 3, 4})
|
||||
tt := []struct {
|
||||
other []byte
|
||||
expected []byte
|
||||
}{
|
||||
{other: []byte{1, 2, 3, 4}, expected: []byte{}},
|
||||
{other: []byte{2, 3, 4}, expected: []byte{1}},
|
||||
{other: []byte{1, 2, 4}, expected: []byte{3}},
|
||||
{other: []byte{4}, expected: []byte{1, 2, 3}},
|
||||
{other: []byte{}, expected: []byte{1, 2, 3, 4}},
|
||||
{other: []byte{1, 2, 3, 4, 5}, expected: []byte{}},
|
||||
{other: []byte{3, 4, 5}, expected: []byte{1, 2}},
|
||||
{other: []byte{5}, expected: []byte{1, 2, 3, 4}},
|
||||
}
|
||||
|
||||
for index, tc := range tt {
|
||||
td := tc
|
||||
t.Run(fmt.Sprintf("%v", index), func(t *testing.T) {
|
||||
other := compression.TilePaletteKeyFrom(td.other)
|
||||
result := baseKey.Without(&other)
|
||||
expected := compression.TilePaletteKeyFrom(td.expected)
|
||||
assert.Equal(t, result, expected, "Mismatch for %v", td.other)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package compression
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// UnpackControlWords reads from an encoded string of bytes a series of packed control words.
|
||||
// If all is OK, the control words are returned. If the function returns an error, something
|
||||
// could not be read/decoded properly.
|
||||
func UnpackControlWords(data []byte) (words []ControlWord, err error) {
|
||||
if len(data) < 4 {
|
||||
return nil, ErrFormat
|
||||
}
|
||||
|
||||
var controlBytes uint32
|
||||
reader := bytes.NewReader(data)
|
||||
|
||||
err = binary.Read(reader, binary.LittleEndian, &controlBytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if controlBytes%bytesPerControlWord != 0 {
|
||||
return nil, ErrFormat
|
||||
}
|
||||
wordCount := int(controlBytes / bytesPerControlWord)
|
||||
unpacked := 0
|
||||
|
||||
words = make([]ControlWord, wordCount)
|
||||
for unpacked < wordCount {
|
||||
var packed PackedControlWord
|
||||
err = binary.Read(reader, binary.LittleEndian, &packed)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
times := packed.Times()
|
||||
if times > len(words)-unpacked {
|
||||
return nil, ErrFormat
|
||||
}
|
||||
for i := 0; i < times; i++ {
|
||||
words[unpacked+i] = packed.Value()
|
||||
}
|
||||
unpacked += times
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package compression_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/inkyblackness/hacked/ss1/content/movie/internal/compression"
|
||||
)
|
||||
|
||||
func TestUnpackControlWordsFormatErrorForEmptyArray(t *testing.T) {
|
||||
_, err := compression.UnpackControlWords(nil)
|
||||
|
||||
assert.Equal(t, compression.ErrFormat, err)
|
||||
}
|
||||
|
||||
func TestUnpackControlWordsFormatErrorForTooSmallSizeField(t *testing.T) {
|
||||
_, err := compression.UnpackControlWords(make([]byte, 3))
|
||||
|
||||
assert.Equal(t, compression.ErrFormat, err)
|
||||
}
|
||||
|
||||
func TestUnpackControlWordsFormatErrorForSizeValueNotMultipleOfThree(t *testing.T) {
|
||||
_, err := compression.UnpackControlWords([]byte{0x02, 0x00, 0x00, 0x00, 0xCC, 0xBB, 0xAA, 0x01})
|
||||
|
||||
assert.Equal(t, compression.ErrFormat, err)
|
||||
}
|
||||
|
||||
func TestUnpackControlWordsEmptyResultForNoEntries(t *testing.T) {
|
||||
words, err := compression.UnpackControlWords([]byte{0x00, 0x00, 0x00, 0x00})
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, 0, len(words))
|
||||
}
|
||||
|
||||
func TestUnpackControlWordsSingleEntry(t *testing.T) {
|
||||
words, err := compression.UnpackControlWords([]byte{0x03, 0x00, 0x00, 0x00, 0xCC, 0xBB, 0xAA, 0x01})
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, []compression.ControlWord{compression.ControlWord(0xAABBCC)}, words)
|
||||
}
|
||||
|
||||
func TestUnpackControlWordsMultipleEntry(t *testing.T) {
|
||||
words, err := compression.UnpackControlWords([]byte{0x09, 0x00, 0x00, 0x00, 0xCC, 0xBB, 0xAA, 0x02, 0x33, 0x22, 0x11, 0x01})
|
||||
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, []compression.ControlWord{compression.ControlWord(0xAABBCC), compression.ControlWord(0xAABBCC), compression.ControlWord(0x112233)}, words)
|
||||
}
|
||||
|
||||
func TestUnpackControlWordsErrorForTooManyUnpacked(t *testing.T) {
|
||||
_, err := compression.UnpackControlWords([]byte{0x03, 0x00, 0x00, 0x00, 0xCC, 0xBB, 0xAA, 0x02})
|
||||
|
||||
require.NotNil(t, err)
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user