2021SC@SDUSC Application and practice of software engineering in Software College of Shandong University -- Ebiten code analysis and source code analysis

Keywords: Go

2021SC@SDUSC

1, Overview

This paper will mainly describe the related methods in packing package in ebiten. Packing package provides a two-dimensional space packing algorithm. The file is located in the internal/packing/packing.go folder.

2, Code analysis

const (
	minSize = 1
)

type Page struct {
	root    *Node
	size    int
	maxSize int

	rollbackExtension func()
}

func NewPage(initSize int, maxSize int) *Page {
	return &Page{
		size:    initSize,
		maxSize: maxSize,
	}
}

func (p *Page) IsEmpty() bool {
	if p.root == nil {
		return true
	}
	return !p.root.used && p.root.child0 == nil && p.root.child1 == nil
}

type Node struct {
	x      int
	y      int
	width  int
	height int
	used   bool

	parent *Node
	child0 *Node
	child1 *Node
}

First, define a page class with three attributes: root node, current size and maximum capacity, and a rollback extension method. Then, create a new page method and judge whether the page is empty.
Next, the Node node class corresponding to the root attribute in the page class is defined.
There are x,y,width,height,used,parent,child1,child0 attributes.
Then is the method to judge whether the Node can be released:

func (n *Node) canFree() bool {
	if n.used {
		return false
	}
	if n.child0 == nil && n.child1 == nil {
		return true
	}
	return n.child0.canFree() && n.child1.canFree()
}

func (n *Node) Region() (x, y, width, height int) {
	return n.x, n.y, n.width, n.height
}

If the used attribute of the node is true, it cannot be released. If both child nodes of the node are empty, it can be released. If the above conditions are not met, both bytes of the node can be released, and it can also be released itself.

The Region method is to obtain the X, y, width and height attributes of the Node.

Next are the square method and the alloc method:

//Square returns a floating-point value representing the distance between a given rectangle and a square.
//Returns 1 (maximum) if the given rectangle is a square.
//Otherwise, the value in [0, 1] is returned.
func square(width, height int) float64 {
	if width == 0 && height == 0 {
		return 0
	}
	if width <= height {
		return float64(width) / float64(height)
	}
	return float64(height) / float64(width)
}

func (p *Page) alloc(n *Node, width, height int) *Node {
	if n.width < width || n.height < height {
		return nil
	}
	if n.used {
		return nil
	}
	if n.child0 == nil && n.child1 == nil {
		if n.width == width && n.height == height {
			n.used = true
			return n
		}
		if square(n.width-width, n.height) >= square(n.width, n.height-height) {
			//Split Vertically 
			n.child0 = &Node{
				x:      n.x,
				y:      n.y,
				width:  width,
				height: n.height,
				parent: n,
			}
			n.child1 = &Node{
				x:      n.x + width,
				y:      n.y,
				width:  n.width - width,
				height: n.height,
				parent: n,
			}
		} else {
			//Overall split
			n.child0 = &Node{
				x:      n.x,
				y:      n.y,
				width:  n.width,
				height: height,
				parent: n,
			}
			n.child1 = &Node{
				x:      n.x,
				y:      n.y + height,
				width:  n.width,
				height: n.height - height,
				parent: n,
			}
		}
		return p.alloc(n.child0, width, height)
	}
	if n.child0 == nil || n.child1 == nil {
		panic("packing: both two children must not be nil at alloc")
	}
	if node := p.alloc(n.child0, width, height); node != nil {
		return node
	}
	if node := p.alloc(n.child1, width, height); node != nil {
		return node
	}
	return nil
}

The square method obtains the gap between the rectangle and the square according to the width and height of the Node. If the width and height are all 0, it returns 0. Otherwise, it returns the float type result of dividing the shorter edge by the longer edge.

Alloc method is a recursive method. The incoming parameters are node, width and height. First, judge and compare the width and incoming width of the node, the height of the node and the incoming height. If the node attribute is small, nil is returned. If the incoming node has been used, nil is also returned. Next, judge if the two child nodes of the incoming node are empty, Decide whether to perform vertical split or overall split according to the difference between the width and height of the node and the incoming width and height, and then call this method for recursion; If the child nodes are not empty at the same time, the value of calling the alloc method on the child node that is not empty is returned.

Then the size method and the SetMaxSize method:

func (p *Page) Size() int {
	return p.size
}

func (p *Page) SetMaxSize(size int) {
	if p.maxSize > size {
		panic("packing: maxSize cannot be decreased")
	}
	p.maxSize = size
}

Obtain the current size of the page object and set the maximum size of the page object.
Next, the Alloc method and Free method of the page class are defined:

func (p *Page) Alloc(width, height int) *Node {
	if width <= 0 || height <= 0 {
		panic("packing: width and height must > 0")
	}
	if p.root == nil {
		p.root = &Node{
			width:  p.size,
			height: p.size,
		}
	}
	if width < minSize {
		width = minSize
	}
	if height < minSize {
		height = minSize
	}
	n := p.alloc(p.root, width, height)
	return n
}

func (p *Page) Free(node *Node) {
	if node.child0 != nil || node.child1 != nil {
		panic("packing: can't free the node including children")
	}
	node.used = false
	if node.parent == nil {
		return
	}
	if node.parent.child0 == nil || node.parent.child1 == nil {
		panic("packing: both two children must not be nil at Free: double free happened?")
	}
	if node.parent.child0.canFree() && node.parent.child1.canFree() {
		node.parent.child0 = nil
		node.parent.child1 = nil
		p.Free(node.parent)
	}
}

The alloc method encapsulates the alloc method just now so that the page object can be called. I won't repeat it here.

The Free method first determines whether all the child nodes of the incoming node are empty. If not, an error will pop up. Next, set the used attribute of the node to false. If the parent node of the current node is empty, the method will be terminated between. On the contrary, it determines whether there is an empty node in the child node of the parent node of the current node. If both child nodes exist, Then go to the next step to judge whether both child nodes can be released. If both can be released, set both child nodes of the parent node of the current node to null and try to release the parent node.

Then the walk method:

func walk(n *Node, f func(n *Node) error) error {
	if err := f(n); err != nil {
		return err
	}
	if n.child0 != nil {
		if err := walk(n.child0, f); err != nil {
			return err
		}
	}
	if n.child1 != nil {
		if err := walk(n.child1, f); err != nil {
			return err
		}
	}
	return nil
}

The function of the walk method is to pass in a node and a method, traverse the two child nodes of the method, and give priority to the child node 0. If it is not empty, you can execute the method and return the error result.

The following are the Extend extension method and the one-time rollback call method, that is, the submit extension method:

func (p *Page) Extend(count int) bool {
	if p.rollbackExtension != nil {
		panic("packing: Extend cannot be called without rolling back or committing")
	}

	if p.size >= p.maxSize {
		return false
	}

	newSize := p.size
	for i := 0; i < count; i++ {
		newSize *= 2
	}

	if newSize > p.maxSize {
		return false
	}

	edgeNodes := []*Node{}
	abort := errors.New("abort")
	aborted := false
	if p.root != nil {
		_ = walk(p.root, func(n *Node) error {
			if n.x+n.width < p.size && n.y+n.height < p.size {
				return nil
			}
			if n.used {
				aborted = true
				return abort
			}
			edgeNodes = append(edgeNodes, n)
			return nil
		})
	}

	if aborted {
		origRoot := *p.root

		leftUpper := p.root
		leftLower := &Node{
			x:      0,
			y:      p.size,
			width:  p.size,
			height: newSize - p.size,
		}
		left := &Node{
			x:      0,
			y:      0,
			width:  p.size,
			height: p.size,
			child0: leftUpper,
			child1: leftLower,
		}
		leftUpper.parent = left
		leftLower.parent = left

		right := &Node{
			x:      p.size,
			y:      0,
			width:  newSize - p.size,
			height: newSize,
		}
		p.root = &Node{
			x:      0,
			y:      0,
			width:  newSize,
			height: newSize,
			child0: left,
			child1: right,
		}
		left.parent = p.root
		right.parent = p.root

		origSize := p.size
		p.rollbackExtension = func() {
			p.size = origSize
			p.root = &origRoot
		}
	} else {
		origSize := p.size
		origWidths := map[*Node]int{}
		origHeights := map[*Node]int{}

		for _, n := range edgeNodes {
			if n.x+n.width == p.size {
				origWidths[n] = n.width
				n.width += newSize - p.size
			}
			if n.y+n.height == p.size {
				origHeights[n] = n.height
				n.height += newSize - p.size
			}
		}

		p.rollbackExtension = func() {
			p.size = origSize
			for n, w := range origWidths {
				n.width = w
			}
			for n, h := range origHeights {
				n.height = h
			}
		}
	}

	p.size = newSize

	return true
}

//RollbackExtension rolls back the extension call once
func (p *Page) RollbackExtension() {
	if p.rollbackExtension == nil {
		panic("packing: RollbackExtension cannot be called without Extend")
	}
	p.rollbackExtension()
	p.rollbackExtension = nil
}

//The Committee Extension submits the Extension call.
func (p *Page) CommitExtension() {
	if p.rollbackExtension == nil {
		panic("packing: RollbackExtension cannot be called without Extend")
	}
	p.rollbackExtension = nil
}

The pass in parameter of the Extend method is the number of extensions. Continuously expand the size of the newSize. At the same time, note that the newSize cannot be larger than maxSize, and then update the root attribute according to the newSize played by the extension.
The rollback extension method is called once when the rollbackExtension attribute of page is not empty, and set it to empty after calling.
The submission extension method determines that when the rollbackExtension attribute of page is not empty, it will be set to empty.

Posted by bouwob on Sun, 05 Dec 2021 10:49:08 -0800