mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
49296bd0ca
4GB of render data should be sufficient for anyone.
By replacing an int with uint32, it allows for a smaller memory
footprint and faster caching. Example impact on rendering static
labels.
│ before.txt │ after.txt │
│ sec/op │ sec/op vs base │
LabelStatic/1000runes-RTL-arabic-32 98.08µ ± 3% 88.17µ ± 1% -10.10% (p=0.002 n=6)
LabelStatic/1000runes-RTL-complex-32 103.9µ ± 2% 101.9µ ± 1% -1.84% (p=0.009 n=6)
LabelStatic/1000runes-RTL-emoji-32 113.3µ ± 2% 100.7µ ± 3% -11.11% (p=0.002 n=6)
LabelStatic/1000runes-RTL-latin-32 100.01µ ± 1% 92.31µ ± 1% -7.69% (p=0.002 n=6)
LabelStatic/1000runes-LTR-arabic-32 97.90µ ± 2% 87.92µ ± 2% -10.19% (p=0.002 n=6)
LabelStatic/1000runes-LTR-complex-32 102.63µ ± 2% 99.81µ ± 1% -2.75% (p=0.002 n=6)
LabelStatic/1000runes-LTR-emoji-32 106.56µ ± 2% 98.47µ ± 1% -7.59% (p=0.002 n=6)
LabelStatic/1000runes-LTR-latin-32 97.51µ ± 1% 92.60µ ± 3% -5.03% (p=0.002 n=6)
geomean 102.4µ 95.09µ -7.10%
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
548 lines
14 KiB
Go
548 lines
14 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package ops
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"image"
|
|
"math"
|
|
|
|
"gioui.org/f32"
|
|
"gioui.org/internal/byteslice"
|
|
"gioui.org/internal/scene"
|
|
)
|
|
|
|
type Ops struct {
|
|
// version is incremented at each Reset.
|
|
version uint32
|
|
// data contains the serialized operations.
|
|
data []byte
|
|
// refs hold external references for operations.
|
|
refs []interface{}
|
|
// stringRefs provides space for string references, pointers to which will
|
|
// be stored in refs. Storing a string directly in refs would cause a heap
|
|
// allocation, to store the string header in an interface value. The backing
|
|
// array of stringRefs, on the other hand, gets reused between calls to
|
|
// reset, making string references free on average.
|
|
//
|
|
// Appending to stringRefs might reallocate the backing array, which will
|
|
// leave pointers to the old array in refs. This temporarily causes a slight
|
|
// increase in memory usage, but this, too, amortizes away as the capacity
|
|
// of stringRefs approaches its stable maximum.
|
|
stringRefs []string
|
|
// nextStateID is the id allocated for the next
|
|
// StateOp.
|
|
nextStateID uint32
|
|
// multipOp indicates a multi-op such as clip.Path is being added.
|
|
multipOp bool
|
|
|
|
macroStack stack
|
|
stacks [_StackKind]stack
|
|
}
|
|
|
|
type OpType byte
|
|
|
|
type Shape byte
|
|
|
|
// Start at a high number for easier debugging.
|
|
const firstOpIndex = 200
|
|
|
|
const (
|
|
TypeMacro OpType = iota + firstOpIndex
|
|
TypeCall
|
|
TypeDefer
|
|
TypeTransform
|
|
TypePopTransform
|
|
TypePushOpacity
|
|
TypePopOpacity
|
|
TypeInvalidate
|
|
TypeImage
|
|
TypePaint
|
|
TypeColor
|
|
TypeLinearGradient
|
|
TypePass
|
|
TypePopPass
|
|
TypePointerInput
|
|
TypeClipboardRead
|
|
TypeClipboardWrite
|
|
TypeSource
|
|
TypeTarget
|
|
TypeOffer
|
|
TypeKeyInput
|
|
TypeKeyFocus
|
|
TypeKeySoftKeyboard
|
|
TypeSave
|
|
TypeLoad
|
|
TypeAux
|
|
TypeClip
|
|
TypePopClip
|
|
TypeProfile
|
|
TypeCursor
|
|
TypePath
|
|
TypeStroke
|
|
TypeSemanticLabel
|
|
TypeSemanticDesc
|
|
TypeSemanticClass
|
|
TypeSemanticSelected
|
|
TypeSemanticEnabled
|
|
TypeSnippet
|
|
TypeSelection
|
|
TypeActionInput
|
|
)
|
|
|
|
type StackID struct {
|
|
id uint32
|
|
prev uint32
|
|
}
|
|
|
|
// StateOp represents a saved operation snapshot to be restored
|
|
// later.
|
|
type StateOp struct {
|
|
id uint32
|
|
macroID uint32
|
|
ops *Ops
|
|
}
|
|
|
|
// stack tracks the integer identities of stack operations to ensure correct
|
|
// pairing of their push and pop methods.
|
|
type stack struct {
|
|
currentID uint32
|
|
nextID uint32
|
|
}
|
|
|
|
type StackKind uint8
|
|
|
|
// ClipOp is the shadow of clip.Op.
|
|
type ClipOp struct {
|
|
Bounds image.Rectangle
|
|
Outline bool
|
|
Shape Shape
|
|
}
|
|
|
|
const (
|
|
ClipStack StackKind = iota
|
|
TransStack
|
|
PassStack
|
|
OpacityStack
|
|
_StackKind
|
|
)
|
|
|
|
const (
|
|
Path Shape = iota
|
|
Ellipse
|
|
Rect
|
|
)
|
|
|
|
const (
|
|
TypeMacroLen = 1 + 4 + 4
|
|
TypeCallLen = 1 + 4 + 4 + 4 + 4
|
|
TypeDeferLen = 1
|
|
TypeTransformLen = 1 + 1 + 4*6
|
|
TypePopTransformLen = 1
|
|
TypePushOpacityLen = 1 + 4
|
|
TypePopOpacityLen = 1
|
|
TypeRedrawLen = 1 + 8
|
|
TypeImageLen = 1
|
|
TypePaintLen = 1
|
|
TypeColorLen = 1 + 4
|
|
TypeLinearGradientLen = 1 + 8*2 + 4*2
|
|
TypePassLen = 1
|
|
TypePopPassLen = 1
|
|
TypePointerInputLen = 1 + 1 + 1*2 + 2*4 + 2*4
|
|
TypeClipboardReadLen = 1
|
|
TypeClipboardWriteLen = 1
|
|
TypeSourceLen = 1
|
|
TypeTargetLen = 1
|
|
TypeOfferLen = 1
|
|
TypeKeyInputLen = 1 + 1
|
|
TypeKeyFocusLen = 1 + 1
|
|
TypeKeySoftKeyboardLen = 1 + 1
|
|
TypeSaveLen = 1 + 4
|
|
TypeLoadLen = 1 + 4
|
|
TypeAuxLen = 1
|
|
TypeClipLen = 1 + 4*4 + 1 + 1
|
|
TypePopClipLen = 1
|
|
TypeProfileLen = 1
|
|
TypeCursorLen = 2
|
|
TypePathLen = 8 + 1
|
|
TypeStrokeLen = 1 + 4
|
|
TypeSemanticLabelLen = 1
|
|
TypeSemanticDescLen = 1
|
|
TypeSemanticClassLen = 2
|
|
TypeSemanticSelectedLen = 2
|
|
TypeSemanticEnabledLen = 2
|
|
TypeSnippetLen = 1 + 4 + 4
|
|
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
|
|
TypeActionInputLen = 1 + 1
|
|
)
|
|
|
|
func (op *ClipOp) Decode(data []byte) {
|
|
if len(data) < TypeClipLen || OpType(data[0]) != TypeClip {
|
|
panic("invalid op")
|
|
}
|
|
data = data[:TypeClipLen]
|
|
bo := binary.LittleEndian
|
|
op.Bounds.Min.X = int(int32(bo.Uint32(data[1:])))
|
|
op.Bounds.Min.Y = int(int32(bo.Uint32(data[5:])))
|
|
op.Bounds.Max.X = int(int32(bo.Uint32(data[9:])))
|
|
op.Bounds.Max.Y = int(int32(bo.Uint32(data[13:])))
|
|
op.Outline = data[17] == 1
|
|
op.Shape = Shape(data[18])
|
|
}
|
|
|
|
func Reset(o *Ops) {
|
|
o.macroStack = stack{}
|
|
o.stacks = [_StackKind]stack{}
|
|
// Leave references to the GC.
|
|
for i := range o.refs {
|
|
o.refs[i] = nil
|
|
}
|
|
for i := range o.stringRefs {
|
|
o.stringRefs[i] = ""
|
|
}
|
|
o.data = o.data[:0]
|
|
o.refs = o.refs[:0]
|
|
o.stringRefs = o.stringRefs[:0]
|
|
o.nextStateID = 0
|
|
o.version++
|
|
}
|
|
|
|
func Write(o *Ops, n int) []byte {
|
|
if o.multipOp {
|
|
panic("cannot mix multi ops with single ones")
|
|
}
|
|
o.data = append(o.data, make([]byte, n)...)
|
|
return o.data[len(o.data)-n:]
|
|
}
|
|
|
|
func BeginMulti(o *Ops) {
|
|
if o.multipOp {
|
|
panic("cannot interleave multi ops")
|
|
}
|
|
o.multipOp = true
|
|
}
|
|
|
|
func EndMulti(o *Ops) {
|
|
if !o.multipOp {
|
|
panic("cannot end non multi ops")
|
|
}
|
|
o.multipOp = false
|
|
}
|
|
|
|
func WriteMulti(o *Ops, n int) []byte {
|
|
if !o.multipOp {
|
|
panic("cannot use multi ops in single ops")
|
|
}
|
|
o.data = append(o.data, make([]byte, n)...)
|
|
return o.data[len(o.data)-n:]
|
|
}
|
|
|
|
func PushMacro(o *Ops) StackID {
|
|
return o.macroStack.push()
|
|
}
|
|
|
|
func PopMacro(o *Ops, id StackID) {
|
|
o.macroStack.pop(id)
|
|
}
|
|
|
|
func FillMacro(o *Ops, startPC PC) {
|
|
pc := PCFor(o)
|
|
// Fill out the macro definition reserved in Record.
|
|
data := o.data[startPC.data:]
|
|
data = data[:TypeMacroLen]
|
|
data[0] = byte(TypeMacro)
|
|
bo := binary.LittleEndian
|
|
bo.PutUint32(data[1:], uint32(pc.data))
|
|
bo.PutUint32(data[5:], uint32(pc.refs))
|
|
}
|
|
|
|
func AddCall(o *Ops, callOps *Ops, pc PC, end PC) {
|
|
data := Write1(o, TypeCallLen, callOps)
|
|
data[0] = byte(TypeCall)
|
|
bo := binary.LittleEndian
|
|
bo.PutUint32(data[1:], uint32(pc.data))
|
|
bo.PutUint32(data[5:], uint32(pc.refs))
|
|
bo.PutUint32(data[9:], uint32(end.data))
|
|
bo.PutUint32(data[13:], uint32(end.refs))
|
|
}
|
|
|
|
func PushOp(o *Ops, kind StackKind) (StackID, uint32) {
|
|
return o.stacks[kind].push(), o.macroStack.currentID
|
|
}
|
|
|
|
func PopOp(o *Ops, kind StackKind, sid StackID, macroID uint32) {
|
|
if o.macroStack.currentID != macroID {
|
|
panic("stack push and pop must not cross macro boundary")
|
|
}
|
|
o.stacks[kind].pop(sid)
|
|
}
|
|
|
|
func Write1(o *Ops, n int, ref1 interface{}) []byte {
|
|
o.data = append(o.data, make([]byte, n)...)
|
|
o.refs = append(o.refs, ref1)
|
|
return o.data[len(o.data)-n:]
|
|
}
|
|
|
|
func Write1String(o *Ops, n int, ref1 string) []byte {
|
|
o.data = append(o.data, make([]byte, n)...)
|
|
o.stringRefs = append(o.stringRefs, ref1)
|
|
o.refs = append(o.refs, &o.stringRefs[len(o.stringRefs)-1])
|
|
return o.data[len(o.data)-n:]
|
|
}
|
|
|
|
func Write2(o *Ops, n int, ref1, ref2 interface{}) []byte {
|
|
o.data = append(o.data, make([]byte, n)...)
|
|
o.refs = append(o.refs, ref1, ref2)
|
|
return o.data[len(o.data)-n:]
|
|
}
|
|
|
|
func Write2String(o *Ops, n int, ref1 interface{}, ref2 string) []byte {
|
|
o.data = append(o.data, make([]byte, n)...)
|
|
o.stringRefs = append(o.stringRefs, ref2)
|
|
o.refs = append(o.refs, ref1, &o.stringRefs[len(o.stringRefs)-1])
|
|
return o.data[len(o.data)-n:]
|
|
}
|
|
|
|
func Write3(o *Ops, n int, ref1, ref2, ref3 interface{}) []byte {
|
|
o.data = append(o.data, make([]byte, n)...)
|
|
o.refs = append(o.refs, ref1, ref2, ref3)
|
|
return o.data[len(o.data)-n:]
|
|
}
|
|
|
|
func PCFor(o *Ops) PC {
|
|
return PC{data: uint32(len(o.data)), refs: uint32(len(o.refs))}
|
|
}
|
|
|
|
func (s *stack) push() StackID {
|
|
s.nextID++
|
|
sid := StackID{
|
|
id: s.nextID,
|
|
prev: s.currentID,
|
|
}
|
|
s.currentID = s.nextID
|
|
return sid
|
|
}
|
|
|
|
func (s *stack) check(sid StackID) {
|
|
if s.currentID != sid.id {
|
|
panic("unbalanced operation")
|
|
}
|
|
}
|
|
|
|
func (s *stack) pop(sid StackID) {
|
|
s.check(sid)
|
|
s.currentID = sid.prev
|
|
}
|
|
|
|
// Save the effective transformation.
|
|
func Save(o *Ops) StateOp {
|
|
o.nextStateID++
|
|
s := StateOp{
|
|
ops: o,
|
|
id: o.nextStateID,
|
|
macroID: o.macroStack.currentID,
|
|
}
|
|
bo := binary.LittleEndian
|
|
data := Write(o, TypeSaveLen)
|
|
data[0] = byte(TypeSave)
|
|
bo.PutUint32(data[1:], uint32(s.id))
|
|
return s
|
|
}
|
|
|
|
// Load a previously saved operations state given
|
|
// its ID.
|
|
func (s StateOp) Load() {
|
|
bo := binary.LittleEndian
|
|
data := Write(s.ops, TypeLoadLen)
|
|
data[0] = byte(TypeLoad)
|
|
bo.PutUint32(data[1:], uint32(s.id))
|
|
}
|
|
|
|
func DecodeCommand(d []byte) scene.Command {
|
|
var cmd scene.Command
|
|
copy(byteslice.Uint32(cmd[:]), d)
|
|
return cmd
|
|
}
|
|
|
|
func EncodeCommand(out []byte, cmd scene.Command) {
|
|
copy(out, byteslice.Uint32(cmd[:]))
|
|
}
|
|
|
|
func DecodeTransform(data []byte) (t f32.Affine2D, push bool) {
|
|
if OpType(data[0]) != TypeTransform {
|
|
panic("invalid op")
|
|
}
|
|
push = data[1] != 0
|
|
data = data[2:]
|
|
data = data[:4*6]
|
|
|
|
bo := binary.LittleEndian
|
|
a := math.Float32frombits(bo.Uint32(data))
|
|
b := math.Float32frombits(bo.Uint32(data[4*1:]))
|
|
c := math.Float32frombits(bo.Uint32(data[4*2:]))
|
|
d := math.Float32frombits(bo.Uint32(data[4*3:]))
|
|
e := math.Float32frombits(bo.Uint32(data[4*4:]))
|
|
f := math.Float32frombits(bo.Uint32(data[4*5:]))
|
|
return f32.NewAffine2D(a, b, c, d, e, f), push
|
|
}
|
|
|
|
func DecodeOpacity(data []byte) float32 {
|
|
if OpType(data[0]) != TypePushOpacity {
|
|
panic("invalid op")
|
|
}
|
|
bo := binary.LittleEndian
|
|
return math.Float32frombits(bo.Uint32(data[1:]))
|
|
}
|
|
|
|
// DecodeSave decodes the state id of a save op.
|
|
func DecodeSave(data []byte) int {
|
|
if OpType(data[0]) != TypeSave {
|
|
panic("invalid op")
|
|
}
|
|
bo := binary.LittleEndian
|
|
return int(bo.Uint32(data[1:]))
|
|
}
|
|
|
|
// DecodeLoad decodes the state id of a load op.
|
|
func DecodeLoad(data []byte) int {
|
|
if OpType(data[0]) != TypeLoad {
|
|
panic("invalid op")
|
|
}
|
|
bo := binary.LittleEndian
|
|
return int(bo.Uint32(data[1:]))
|
|
}
|
|
|
|
type opProp struct {
|
|
Size byte
|
|
NumRefs byte
|
|
}
|
|
|
|
var opProps = [0x100]opProp{
|
|
TypeMacro: {Size: TypeMacroLen, NumRefs: 0},
|
|
TypeCall: {Size: TypeCallLen, NumRefs: 1},
|
|
TypeDefer: {Size: TypeDeferLen, NumRefs: 0},
|
|
TypeTransform: {Size: TypeTransformLen, NumRefs: 0},
|
|
TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0},
|
|
TypePushOpacity: {Size: TypePushOpacityLen, NumRefs: 0},
|
|
TypePopOpacity: {Size: TypePopOpacityLen, NumRefs: 0},
|
|
TypeInvalidate: {Size: TypeRedrawLen, NumRefs: 0},
|
|
TypeImage: {Size: TypeImageLen, NumRefs: 2},
|
|
TypePaint: {Size: TypePaintLen, NumRefs: 0},
|
|
TypeColor: {Size: TypeColorLen, NumRefs: 0},
|
|
TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0},
|
|
TypePass: {Size: TypePassLen, NumRefs: 0},
|
|
TypePopPass: {Size: TypePopPassLen, NumRefs: 0},
|
|
TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1},
|
|
TypeClipboardRead: {Size: TypeClipboardReadLen, NumRefs: 1},
|
|
TypeClipboardWrite: {Size: TypeClipboardWriteLen, NumRefs: 1},
|
|
TypeSource: {Size: TypeSourceLen, NumRefs: 2},
|
|
TypeTarget: {Size: TypeTargetLen, NumRefs: 2},
|
|
TypeOffer: {Size: TypeOfferLen, NumRefs: 3},
|
|
TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2},
|
|
TypeKeyFocus: {Size: TypeKeyFocusLen, NumRefs: 1},
|
|
TypeKeySoftKeyboard: {Size: TypeKeySoftKeyboardLen, NumRefs: 0},
|
|
TypeSave: {Size: TypeSaveLen, NumRefs: 0},
|
|
TypeLoad: {Size: TypeLoadLen, NumRefs: 0},
|
|
TypeAux: {Size: TypeAuxLen, NumRefs: 0},
|
|
TypeClip: {Size: TypeClipLen, NumRefs: 0},
|
|
TypePopClip: {Size: TypePopClipLen, NumRefs: 0},
|
|
TypeProfile: {Size: TypeProfileLen, NumRefs: 1},
|
|
TypeCursor: {Size: TypeCursorLen, NumRefs: 0},
|
|
TypePath: {Size: TypePathLen, NumRefs: 0},
|
|
TypeStroke: {Size: TypeStrokeLen, NumRefs: 0},
|
|
TypeSemanticLabel: {Size: TypeSemanticLabelLen, NumRefs: 1},
|
|
TypeSemanticDesc: {Size: TypeSemanticDescLen, NumRefs: 1},
|
|
TypeSemanticClass: {Size: TypeSemanticClassLen, NumRefs: 0},
|
|
TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0},
|
|
TypeSemanticEnabled: {Size: TypeSemanticEnabledLen, NumRefs: 0},
|
|
TypeSnippet: {Size: TypeSnippetLen, NumRefs: 2},
|
|
TypeSelection: {Size: TypeSelectionLen, NumRefs: 1},
|
|
TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0},
|
|
}
|
|
|
|
func (t OpType) props() (size, numRefs uint32) {
|
|
v := opProps[t]
|
|
return uint32(v.Size), uint32(v.NumRefs)
|
|
}
|
|
|
|
func (t OpType) Size() uint32 {
|
|
return uint32(opProps[t].Size)
|
|
}
|
|
|
|
func (t OpType) NumRefs() uint32 {
|
|
return uint32(opProps[t].NumRefs)
|
|
}
|
|
|
|
func (t OpType) String() string {
|
|
switch t {
|
|
case TypeMacro:
|
|
return "Macro"
|
|
case TypeCall:
|
|
return "Call"
|
|
case TypeDefer:
|
|
return "Defer"
|
|
case TypeTransform:
|
|
return "Transform"
|
|
case TypePopTransform:
|
|
return "PopTransform"
|
|
case TypePushOpacity:
|
|
return "PushOpacity"
|
|
case TypePopOpacity:
|
|
return "PopOpacity"
|
|
case TypeInvalidate:
|
|
return "Invalidate"
|
|
case TypeImage:
|
|
return "Image"
|
|
case TypePaint:
|
|
return "Paint"
|
|
case TypeColor:
|
|
return "Color"
|
|
case TypeLinearGradient:
|
|
return "LinearGradient"
|
|
case TypePass:
|
|
return "Pass"
|
|
case TypePopPass:
|
|
return "PopPass"
|
|
case TypePointerInput:
|
|
return "PointerInput"
|
|
case TypeClipboardRead:
|
|
return "ClipboardRead"
|
|
case TypeClipboardWrite:
|
|
return "ClipboardWrite"
|
|
case TypeSource:
|
|
return "Source"
|
|
case TypeTarget:
|
|
return "Target"
|
|
case TypeOffer:
|
|
return "Offer"
|
|
case TypeKeyInput:
|
|
return "KeyInput"
|
|
case TypeKeyFocus:
|
|
return "KeyFocus"
|
|
case TypeKeySoftKeyboard:
|
|
return "KeySoftKeyboard"
|
|
case TypeSave:
|
|
return "Save"
|
|
case TypeLoad:
|
|
return "Load"
|
|
case TypeAux:
|
|
return "Aux"
|
|
case TypeClip:
|
|
return "Clip"
|
|
case TypePopClip:
|
|
return "PopClip"
|
|
case TypeProfile:
|
|
return "Profile"
|
|
case TypeCursor:
|
|
return "Cursor"
|
|
case TypePath:
|
|
return "Path"
|
|
case TypeStroke:
|
|
return "Stroke"
|
|
case TypeSemanticLabel:
|
|
return "SemanticDescription"
|
|
default:
|
|
panic("unknown OpType")
|
|
}
|
|
}
|