doodle/pkg/level/chunk_rle.go

202 lines
4.7 KiB
Go

package level
import (
"bytes"
"encoding/binary"
"errors"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/go/render"
)
// RLEAccessor implements a chunk accessor which stores its on-disk format using
// Run Length Encoding (RLE), but in memory behaves equivalently to the MapAccessor.
type RLEAccessor struct {
acc *MapAccessor
}
// NewRLEAccessor initializes a RLEAccessor.
func NewRLEAccessor() *RLEAccessor {
return &RLEAccessor{
acc: NewMapAccessor(),
}
}
// SetChunkCoordinate receives our chunk's coordinate from the Chunker.
func (a *RLEAccessor) SetChunkCoordinate(p render.Point, size uint8) {
a.acc.coord = p
a.acc.size = size
}
// Inflate the sparse swatches from their palette indexes.
func (a *RLEAccessor) Inflate(pal *Palette) error {
return a.acc.Inflate(pal)
}
// Len returns the current size of the map, or number of pixels registered.
func (a *RLEAccessor) Len() int {
return a.acc.Len()
}
// IterViewport returns a channel to loop over pixels in the viewport.
func (a *RLEAccessor) IterViewport(viewport render.Rect) <-chan Pixel {
return a.acc.IterViewport(viewport)
}
// Iter returns a channel to loop over all points in this chunk.
func (a *RLEAccessor) Iter() <-chan Pixel {
return a.acc.Iter()
}
// Get a pixel from the map.
func (a *RLEAccessor) Get(p render.Point) (*Swatch, error) {
return a.acc.Get(p)
}
// Set a pixel on the map.
func (a *RLEAccessor) Set(p render.Point, sw *Swatch) error {
return a.acc.Set(p, sw)
}
// Delete a pixel from the map.
func (a *RLEAccessor) Delete(p render.Point) error {
return a.acc.Delete(p)
}
// Make2DChunkGrid creates a 2D map of uint64 pointers matching the square dimensions of the given size.
//
// It is used by the RLEAccessor to flatten a chunk into a grid for run-length encoding.
func Make2DChunkGrid(size int) ([][]*uint64, error) {
// Sanity check if the chunk was properly initialized.
if size == 0 {
return nil, errors.New("chunk not initialized correctly with its size and coordinate")
}
var grid = make([][]*uint64, size)
for i := 0; i < size; i++ {
grid[i] = make([]*uint64, size)
}
return grid, nil
}
/*
MarshalBinary converts the chunk data to a binary representation.
This accessor uses Run Length Encoding (RLE) in its binary format. Starting
with the top-left pixel of this chunk, the binary format is a stream of bytes
formatted as such:
- UVarint for the palette index number (0-255), with 0xFF meaning void
- UVarint for the length of repetition of that palette index
*/
func (a *RLEAccessor) MarshalBinary() ([]byte, error) {
// Flatten the chunk out into a full 2D array of all its points.
var (
size = int(a.acc.size)
grid, err = Make2DChunkGrid(size)
)
if err != nil {
return nil, err
}
// Populate the dense 2D array of its pixels.
for px := range a.Iter() {
var (
point = render.NewPoint(px.X, px.Y)
relative = RelativeCoordinate(point, a.acc.coord, a.acc.size)
ptr = uint64(px.PaletteIndex)
)
grid[relative.Y][relative.X] = &ptr
}
// log.Error("2D GRID:\n%+v", grid)
// Run-length encode the grid.
var (
compressed []byte
firstColor = true
lastColor uint64
runLength uint64
)
for _, row := range grid {
for _, color := range row {
var index uint64
if color == nil {
index = 0xFF
}
if firstColor {
lastColor = index
runLength = 1
firstColor = false
continue
}
if index != lastColor {
compressed = binary.AppendUvarint(compressed, index)
compressed = binary.AppendUvarint(compressed, runLength)
lastColor = index
runLength = 1
continue
}
runLength++
}
}
log.Error("RLE compressed: %v", compressed)
return compressed, nil
}
// UnmarshalBinary will decode a compressed RLEAccessor byte stream.
func (a *RLEAccessor) UnmarshalBinary(compressed []byte) error {
a.acc.mu.Lock()
defer a.acc.mu.Unlock()
// New format: decompress the byte stream.
log.Debug("RLEAccessor.Unmarshal: Reading %d bytes of compressed chunk data", len(compressed))
// Prepare the 2D grid to decompress the RLE stream into.
var (
size = int(a.acc.size)
_, err = Make2DChunkGrid(size)
x, y, cursor int
)
if err != nil {
return err
}
var reader = bytes.NewBuffer(compressed)
for {
var (
paletteIndex, err1 = binary.ReadUvarint(reader)
repeatCount, err2 = binary.ReadUvarint(reader)
)
if err1 != nil || err2 != nil {
log.Error("reading Uvarints from compressed data: {%s, %s}", err1, err2)
break
}
for i := uint64(0); i < repeatCount; i++ {
cursor++
if cursor%size == 0 {
y++
x = 0
} else {
x++
}
point := render.NewPoint(int(x), int(y))
if paletteIndex != 0xFF {
a.acc.grid[point] = NewSparseSwatch(int(paletteIndex))
}
}
}
return nil
}