Adding modding folder with a hacked build.

This commit is contained in:
ml
2026-09-26 08:53:08 +02:00
parent cdb960dc71
commit c91080aba7
755 changed files with 50424 additions and 0 deletions
@@ -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)
}

Some files were not shown because too many files have changed in this diff Show More