181 lines
4.9 KiB
Go
181 lines
4.9 KiB
Go
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)
|
|
}
|