// SPDX-License-Identifier: Unlicense OR MIT // Binary op encoding, reader, and decoder. // Ported from gioui.org/internal/ops. package gio import ( "encoding/binary" "image" "math" ) type Ops struct { // version is incremented at each ResetOps. version uint32 // data contains the serialized operations. data []byte // refs hold external references for operations. refs []any // 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. 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 opsStack stacks [_StackKind]opsStack } 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 TypeImage TypePaint TypeColor TypeLinearGradient TypePass TypePopPass TypeInput TypeKeyInputHint TypeSave TypeLoad TypeAux TypeClip TypePopClip TypeCursor TypePath TypeStroke TypeSemanticLabel TypeSemanticDesc TypeSemanticClass TypeSemanticSelected TypeSemanticEnabled 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 } // opsStack tracks the integer identities of stack operations to ensure correct // pairing of their push and pop methods. type opsStack struct { currentID uint32 nextID uint32 } type StackKind uint8 // DecodedClipOp is the decode-side shadow of the public ClipOp. type DecodedClipOp struct { Bounds image.Rectangle Outline bool Shape Shape } const ( ClipStackKind StackKind = iota TransStack PassStackKind OpacityStackKind _StackKind ) const ( ShapePath Shape = iota ShapeEllipse ShapeRect ) 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 + 1 TypePaintLen = 1 TypeColorLen = 1 + 4 TypeLinearGradientLen = 1 + 8*2 + 4*2 TypePassLen = 1 TypePopPassLen = 1 TypeInputLen = 1 TypeKeyInputHintLen = 1 + 1 TypeSaveLen = 1 + 4 TypeLoadLen = 1 + 4 TypeAuxLen = 1 TypeClipLen = 1 + 4*4 + 1 + 1 TypePopClipLen = 1 TypeCursorLen = 2 TypePathLen = 8 + 1 TypeStrokeLen = 1 + 4 TypeSemanticLabelLen = 1 TypeSemanticDescLen = 1 TypeSemanticClassLen = 2 TypeSemanticSelectedLen = 2 TypeSemanticEnabledLen = 2 TypeActionInputLen = 1 + 1 ) func (op *DecodedClipOp) 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 ResetOps(o *Ops) { o.macroStack = opsStack{} o.stacks = [_StackKind]opsStack{} // 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 WriteOps(o *Ops, n int) []byte { if o.multipOp { panic("cannot mix multi ops with single ones") } o.data = append(o.data, []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, []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:], pc.data) bo.PutUint32(data[5:], pc.refs) } func AddCall(o *Ops, callOps *Ops, pc PC, end PC) { data := WriteOps1(o, TypeCallLen, callOps) data[0] = byte(TypeCall) bo := binary.LittleEndian() bo.PutUint32(data[1:], pc.data) bo.PutUint32(data[5:], pc.refs) bo.PutUint32(data[9:], end.data) bo.PutUint32(data[13:], 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 WriteOps1(o *Ops, n int, ref1 any) []byte { o.data = append(o.data, []byte{:n}...) o.refs = append(o.refs, ref1) return o.data[len(o.data)-n:] } func WriteOps1String(o *Ops, n int, ref1 string) []byte { o.data = append(o.data, []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 WriteOps2(o *Ops, n int, ref1, ref2 any) []byte { o.data = append(o.data, []byte{:n}...) o.refs = append(o.refs, ref1, ref2) return o.data[len(o.data)-n:] } func WriteOps2String(o *Ops, n int, ref1 any, ref2 string) []byte { o.data = append(o.data, []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 WriteOps3(o *Ops, n int, ref1, ref2, ref3 any) []byte { o.data = append(o.data, []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 *opsStack) push() StackID { s.nextID++ sid := StackID{ id: s.nextID, prev: s.currentID, } s.currentID = s.nextID return sid } func (s *opsStack) check(sid StackID) { if s.currentID != sid.id { panic("unbalanced operation") } } func (s *opsStack) pop(sid StackID) { s.check(sid) s.currentID = sid.prev } // SaveOps saves the effective transformation. func SaveOps(o *Ops) StateOp { o.nextStateID++ s := StateOp{ ops: o, id: o.nextStateID, macroID: o.macroStack.currentID, } bo := binary.LittleEndian() data := WriteOps(o, TypeSaveLen) data[0] = byte(TypeSave) bo.PutUint32(data[1:], s.id) return s } // Load a previously saved operations state given its ID. func (s StateOp) Load() { bo := binary.LittleEndian() data := WriteOps(s.ops, TypeLoadLen) data[0] = byte(TypeLoad) bo.PutUint32(data[1:], s.id) } // DecodeCommand decodes a Command from a byte slice. // Uses Uint32sToBytes for the same host-endian reinterpret as the original. func DecodeCommand(d []byte) Command { var cmd Command copy(Uint32sToBytes(cmd[:]), d) return cmd } // EncodeCommand encodes a Command into a byte slice. func EncodeCommand(out []byte, cmd Command) { copy(out, Uint32sToBytes(cmd[:])) } func DecodeTransform(data []byte) (t 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 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 } func opProps() [0x100]opProp { return [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}, 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}, TypeInput: {Size: TypeInputLen, NumRefs: 1}, TypeKeyInputHint: {Size: TypeKeyInputHintLen, NumRefs: 1}, 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}, 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}, 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 TypeImage: return "Image" case TypePaint: return "Paint" case TypeColor: return "Color" case TypeLinearGradient: return "LinearGradient" case TypePass: return "Pass" case TypePopPass: return "PopPass" case TypeInput: return "Input" case TypeKeyInputHint: return "KeyInputHint" case TypeSave: return "Save" case TypeLoad: return "Load" case TypeAux: return "Aux" case TypeClip: return "Clip" case TypePopClip: return "PopClip" case TypeCursor: return "Cursor" case TypePath: return "Path" case TypeStroke: return "Stroke" case TypeSemanticLabel: return "SemanticDescription" default: panic("unknown OpType") } } // --- Reader (from reader.go) --- // Reader parses an ops list. type Reader struct { pc PC stack []readerMacro ops *Ops deferOps Ops deferDone bool } // EncodedOp represents an encoded op returned by Reader. type EncodedOp struct { Key Key Data []byte Refs []any } // Key is a unique key for a given op. type Key struct { ops *Ops pc uint32 version uint32 } // Shadow of op.MacroOp. type macroOp struct { ops *Ops start PC end PC } // PC is an instruction counter for an operation list. type PC struct { data uint32 refs uint32 } type readerMacro struct { ops *Ops retPC PC endPC PC } type opMacroDef struct { endpc PC } func (pc PC) Add(op OpType) PC { size, numRefs := op.props() return PC{ data: pc.data + size, refs: pc.refs + numRefs, } } // ResetReader starts reading from the beginning of ops. func (r *Reader) ResetReader(ops *Ops) { r.ResetAt(ops, PC{}) } // ResetAt is like ResetReader, except it starts reading from pc. func (r *Reader) ResetAt(ops *Ops, pc PC) { r.stack = r.stack[:0] ResetOps(&r.deferOps) r.deferDone = false r.pc = pc r.ops = ops } func (r *Reader) Decode() (EncodedOp, bool) { if r.ops == nil { return EncodedOp{}, false } deferring := false for { if len(r.stack) > 0 { b := r.stack[len(r.stack)-1] if r.pc == b.endPC { r.ops = b.ops r.pc = b.retPC r.stack = r.stack[:len(r.stack)-1] continue } } data := r.ops.data data = data[r.pc.data:] refs := r.ops.refs if len(data) == 0 { if r.deferDone { return EncodedOp{}, false } r.deferDone = true // Execute deferred macros. r.ops = &r.deferOps r.pc = PC{} continue } key := Key{ops: r.ops, pc: r.pc.data, version: r.ops.version} t := OpType(data[0]) n, nrefs := t.props() data = data[:n] refs = refs[r.pc.refs:] refs = refs[:nrefs] switch t { case TypeDefer: deferring = true r.pc.data += n r.pc.refs += nrefs continue case TypeAux: // An Aux operation is always wrapped in a macro, and // its length is the remaining space. block := r.stack[len(r.stack)-1] n += block.endPC.data - r.pc.data - TypeAuxLen data = data[:n] case TypeCall: if deferring { deferring = false // Copy macro for deferred execution. if nrefs != 1 { panic("internal error: unexpected number of macro refs") } deferData := WriteOps1(&r.deferOps, int(n), refs[0]) copy(deferData, data) r.pc.data += n r.pc.refs += nrefs continue } var op macroOp op.decode(data, refs) retPC := r.pc retPC.data += n retPC.refs += nrefs r.stack = append(r.stack, readerMacro{ ops: r.ops, retPC: retPC, endPC: op.end, }) r.ops = op.ops r.pc = op.start continue case TypeMacro: var op opMacroDef op.decode(data) if op.endpc != (PC{}) { r.pc = op.endpc } else { // Treat an incomplete macro as containing all remaining ops. r.pc.data = uint32(len(r.ops.data)) r.pc.refs = uint32(len(r.ops.refs)) } continue } r.pc.data += n r.pc.refs += nrefs return EncodedOp{Key: key, Data: data, Refs: refs}, true } } func (op *opMacroDef) decode(data []byte) { if len(data) < TypeMacroLen || OpType(data[0]) != TypeMacro { panic("invalid op") } bo := binary.LittleEndian() data = data[:TypeMacroLen] op.endpc.data = bo.Uint32(data[1:]) op.endpc.refs = bo.Uint32(data[5:]) } func (m *macroOp) decode(data []byte, refs []any) { if len(data) < TypeCallLen || len(refs) < 1 || OpType(data[0]) != TypeCall { panic("invalid op") } bo := binary.LittleEndian() data = data[:TypeCallLen] m.ops = refs[0].(*Ops) m.start.data = bo.Uint32(data[1:]) m.start.refs = bo.Uint32(data[5:]) m.end.data = bo.Uint32(data[9:]) m.end.refs = bo.Uint32(data[13:]) }