| // Copyright (C) 2015 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // Package builder contains the Builder type to build replay payloads. |
| package builder |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| |
| "android.googlesource.com/platform/tools/gpu/atom" |
| "android.googlesource.com/platform/tools/gpu/binary" |
| "android.googlesource.com/platform/tools/gpu/binary/endian" |
| "android.googlesource.com/platform/tools/gpu/binary/flat" |
| "android.googlesource.com/platform/tools/gpu/config" |
| "android.googlesource.com/platform/tools/gpu/device" |
| "android.googlesource.com/platform/tools/gpu/interval" |
| "android.googlesource.com/platform/tools/gpu/log" |
| "android.googlesource.com/platform/tools/gpu/memory" |
| "android.googlesource.com/platform/tools/gpu/replay/asm" |
| "android.googlesource.com/platform/tools/gpu/replay/protocol" |
| "android.googlesource.com/platform/tools/gpu/replay/value" |
| ) |
| |
| type stackItem struct { |
| ty protocol.Type // Type of the item. |
| idx int // Index of the op that generated this. |
| } |
| |
| type marker struct { |
| instruction int // first instruction index for this marker |
| atom atom.ID // the atom identifier |
| } |
| |
| // ResponseDecoder decodes all postback responses from the replay virtual machine. |
| // If err is nil, then r is the Reader to the sequential postback data. If r is nil, |
| // then the postback data was absent or corrupted and err holds the error. |
| type ResponseDecoder func(r io.Reader, err error) |
| |
| // Postback decodes a single atom's postback, returning and carrying over errors. |
| // The Postback must decode all the data that was issued in the Post call before |
| // returning. If err is nil, then d is the Decoder to the postback data. If d is nil, |
| // then a previous postback failed to decode before decoding could begin for this |
| // postback and err holds the error. |
| type Postback func(d binary.Decoder, err error) error |
| |
| // Builder is used to build the Payload to send to the replay virtual machine. |
| // The builder has a number of methods for mutating the virtual machine stack, |
| // invoking functions and posting back data. |
| type Builder struct { |
| constantMemory *constantEncoder |
| heap, temp allocator |
| resourceIDToIdx map[binary.ID]uint32 |
| resources []protocol.ResourceInfo |
| mappedMemory memory.RangeList |
| instructions []asm.Instruction |
| decoders []Postback |
| stack []stackItem |
| architecture device.Architecture |
| inAtom bool // true if between BeginAtom and CommitAtom/RevertAtom |
| atomStart int // index of current atom's first instruction |
| |
| // Remappings is a map of a arbitrary keys to pointers. Typically, this is |
| // used as a map of observed values to values that are only known at replay |
| // execution time, such as driver generated handles. |
| // The Remappings field is not accessed by the Builder and can be used in any |
| // way the developer requires. |
| Remappings map[interface{}]value.Pointer |
| } |
| |
| // New returns a newly constructed Builder configured to replay on a target |
| // with the specified Architecture. |
| func New(architecture device.Architecture) *Builder { |
| return &Builder{ |
| constantMemory: newConstantEncoder(architecture), |
| heap: allocator{alignment: uint64(architecture.PointerAlignment)}, |
| temp: allocator{alignment: uint64(architecture.PointerAlignment)}, |
| resourceIDToIdx: map[binary.ID]uint32{}, |
| resources: []protocol.ResourceInfo{}, |
| mappedMemory: memory.RangeList{}, |
| instructions: []asm.Instruction{}, |
| architecture: architecture, |
| Remappings: make(map[interface{}]value.Pointer), |
| } |
| } |
| |
| func (b *Builder) pushStack(t protocol.Type) { |
| b.stack = append(b.stack, stackItem{t, len(b.instructions)}) |
| } |
| |
| func (b *Builder) popStack() { |
| b.stack = b.stack[:len(b.stack)-1] |
| } |
| |
| func (b *Builder) removeInstruction(at int) { |
| if at == len(b.instructions)-1 { |
| b.instructions = b.instructions[:at] |
| } else { |
| b.instructions[at] = asm.Nop{} |
| } |
| } |
| |
| // Architecture returns the architecture for the target replay device. |
| func (b *Builder) Architecture() device.Architecture { |
| return b.architecture |
| } |
| |
| // AllocateMemory allocates and returns a pointer to a block of memory in the |
| // volatile address-space big enough to hold size bytes. The memory will be |
| // allocated for the entire replay duration and cannot be freed. |
| func (b *Builder) AllocateMemory(size uint64) value.Pointer { |
| return value.VolatilePointer(b.heap.alloc(size)) |
| } |
| |
| // AllocateTemporaryMemory allocates and returns a pointer to a block of memory |
| // in the temporary volatile address-space big enough to hold size bytes. The |
| // memory block will be freed on the next call to EndAtom, upon which reading or |
| // writing to this memory will result in undefined behavior. |
| func (b *Builder) AllocateTemporaryMemory(size uint64) value.Pointer { |
| return value.VolatileTemporaryPointer(b.temp.alloc(size)) |
| } |
| |
| // AllocateTemporaryMemoryChunks allocates a contiguous block of memory in the |
| // temporary volatile address-space big enough to hold all the specified chunks |
| // sizes, in sequential order. AllocateTemporaryMemoryChunks returns a pointer |
| // to each of the allocated chunks and the size of the entire allocation. The |
| // allocation block will be freed on the next call to EndAtom, upon which |
| // reading or writing to this memory will result in undefined behavior. |
| func (b *Builder) AllocateTemporaryMemoryChunks(sizes []uint64) (ptrs []value.Pointer, size uint64) { |
| alignment := uint64(b.architecture.PointerAlignment) |
| ptrs = make([]value.Pointer, len(sizes)) |
| for _, s := range sizes { |
| size = align(size, alignment) |
| size += s |
| } |
| base := b.AllocateTemporaryMemory(size) |
| offset := uint64(0) |
| for i, s := range sizes { |
| ptrs[i] = base.Offset(offset) |
| offset += align(s, alignment) |
| } |
| return ptrs, size |
| } |
| |
| // BeginAtom should be called before building any replay instructions. |
| func (b *Builder) BeginAtom(id atom.ID) { |
| if b.inAtom { |
| panic("BeginAtom called while already building an atom") |
| } |
| b.inAtom = true |
| b.atomStart = len(b.instructions) |
| if id <= 0x3ffffff { // Labels have 26 bit values. |
| b.instructions = append(b.instructions, asm.Label{ |
| Value: uint32(id), |
| }) |
| } |
| } |
| |
| // CommitAtom should be called after emitting the commands to replay a single atom. |
| // CommitAtom frees all temporary allocated memory and clears the stack. |
| func (b *Builder) CommitAtom() { |
| if !b.inAtom { |
| panic("CommitAtom called without a call to BeginAtom") |
| } |
| b.inAtom = false |
| b.temp.reset() |
| pop := uint32(len(b.stack)) |
| // Optimise the instructions. |
| for si := len(b.stack) - 1; si >= 0; si-- { |
| s := b.stack[si] |
| switch i := b.instructions[s.idx].(type) { |
| case asm.Call: // Change calls that push an unused return value to discard the value. |
| if i.PushReturn { |
| i.PushReturn = false |
| b.instructions[s.idx] = i |
| pop-- |
| } |
| case asm.Clone, asm.Push, asm.Load: // Remove unused clones, pushes, loads |
| b.instructions[s.idx] = asm.Nop{} |
| pop-- |
| } |
| } |
| // Trim trailing no-ops |
| for len(b.instructions) > 0 { |
| if _, nop := b.instructions[len(b.instructions)-1].(asm.Nop); nop { |
| b.instructions = b.instructions[:len(b.instructions)-1] |
| } else { |
| break |
| } |
| } |
| // Pop any remaining stack values |
| if pop > 0 { |
| b.instructions = append(b.instructions, asm.Pop{Count: pop}) |
| } |
| b.stack = b.stack[:0] |
| } |
| |
| // RevertAtom reverts all the instructions since the last call to BeginAtom. |
| // Any postbacks issued since the last call to BeginAtom will be called with |
| // the error err and a nil decoder. |
| func (b *Builder) RevertAtom(err error) { |
| if !b.inAtom { |
| panic("RevertAtom called without a call to BeginAtom") |
| } |
| b.inAtom = false |
| // TODO: Revert calls to: AllocateMemory, Buffer, String, MapMemory, Write. |
| b.temp.reset() |
| b.stack = b.stack[:0] |
| if len(b.instructions) > 0 { |
| for i := len(b.instructions) - 1; i >= b.atomStart; i-- { |
| switch b.instructions[i].(type) { |
| case asm.Post: |
| idx := len(b.decoders) - 1 |
| b.decoders[idx](nil, err) |
| b.decoders = b.decoders[:idx] |
| } |
| } |
| b.instructions = b.instructions[:b.atomStart] |
| } |
| } |
| |
| // Buffer returns a pointer to a block of memory in holding the count number of |
| // previously pushed values. |
| // If all the values are constant, then the buffer will be held in the constant |
| // address-space, otherwise the buffer will be built in the temporary |
| // address-space. |
| func (b *Builder) Buffer(count int) value.Pointer { |
| pointerSize := b.architecture.PointerSize |
| dynamic := false |
| size := 0 |
| |
| // Examine the stack to see where these values came from |
| for i := 0; i < count; i++ { |
| e := b.stack[len(b.stack)-i-1] |
| op := b.instructions[e.idx] |
| ty := e.ty |
| if _, isPush := op.(asm.Push); !isPush { |
| // Values that have made their way on to the stack from non-constant |
| // sources cannot be put in the constant buffer. |
| dynamic = true |
| } |
| switch ty { |
| case protocol.TypeConstantPointer, protocol.TypeVolatilePointer: |
| // Pointers cannot be put into the constant buffer as they are remapped |
| // by the VM |
| dynamic = true |
| } |
| |
| size += ty.Size(pointerSize) |
| } |
| |
| if dynamic { |
| // At least one of the values was not from a Push() |
| // Build the buffer in temporary memory at replay time. |
| buf := b.AllocateTemporaryMemory(uint64(size)) |
| offset := size |
| for i := 0; i < count; i++ { |
| e := b.stack[len(b.stack)-1] |
| offset -= e.ty.Size(pointerSize) |
| b.Store(buf.Offset(uint64(offset))) |
| } |
| return buf |
| } else { |
| // All the values are constant. |
| // Move the pushed values into a constant memory buffer. |
| values := make([]value.Value, count) |
| for i := 0; i < count; i++ { |
| e := b.stack[len(b.stack)-1] |
| values[count-i-1] = b.instructions[e.idx].(asm.Push).Value |
| b.removeInstruction(e.idx) |
| b.popStack() |
| } |
| return b.constantMemory.writeValues(values...) |
| } |
| } |
| |
| // String returns a pointer to a block of memory in the constant address-space |
| // holding the string s. The string will be stored with a null-terminating byte. |
| func (b *Builder) String(s string) value.Pointer { |
| return b.constantMemory.writeString(s) |
| } |
| |
| // Call will invoke the function f, popping all parameter values previously |
| // pushed to the stack with Push, starting with the first parameter. If f has |
| // a non-void return type, after invoking the function the return value of the |
| // function will be pushed on to the stack. |
| func (b *Builder) Call(f FunctionInfo) { |
| for i := 0; i < f.Parameters; i++ { |
| b.popStack() |
| } |
| push := f.ReturnType != protocol.TypeVoid |
| if push { |
| b.pushStack(f.ReturnType) |
| } |
| b.instructions = append(b.instructions, asm.Call{ |
| PushReturn: push, |
| FunctionID: f.ID, |
| }) |
| } |
| |
| // Copy pops the target address and then the source address from the top of the |
| // stack, and then copies Count bytes from source to target. |
| func (b *Builder) Copy(size uint64) { |
| b.popStack() |
| b.popStack() |
| b.instructions = append(b.instructions, asm.Copy{ |
| Count: size, |
| }) |
| } |
| |
| // Clone makes a copy of the n-th element from the top of the stack and pushes |
| // the copy to the top of the stack. |
| func (b *Builder) Clone(index int) { |
| sidx := len(b.stack) - 1 - index |
| // Change ownership of the top stack value to the clone instruction. |
| b.stack[sidx].idx = len(b.instructions) |
| b.pushStack(b.stack[sidx].ty) |
| b.instructions = append(b.instructions, asm.Clone{ |
| Index: index, |
| }) |
| } |
| |
| // Load loads the value of type ty from addr and then pushes the loaded value to |
| // the top of the stack. |
| func (b *Builder) Load(ty protocol.Type, addr value.Pointer) { |
| if !addr.IsValid() { |
| panic(fmt.Errorf("Pointer address %v is not valid", addr)) |
| } |
| b.pushStack(ty) |
| b.instructions = append(b.instructions, asm.Load{ |
| DataType: ty, |
| Source: addr, |
| }) |
| } |
| |
| // Store pops the value from the top of the stack and writes the value to addr. |
| func (b *Builder) Store(addr value.Pointer) { |
| if !addr.IsValid() { |
| panic(fmt.Errorf("Pointer address %v is not valid", addr)) |
| } |
| b.popStack() |
| b.instructions = append(b.instructions, asm.Store{ |
| Destination: addr, |
| }) |
| } |
| |
| // Strcpy pops the source address then the target address from the top of the |
| // stack, and then copies at most maxCount-1 bytes from source to target. If |
| // maxCount is greater than the source string length, then the target will be |
| // padded with 0s. The destination buffer will always be 0-terminated. |
| func (b *Builder) Strcpy(maxCount uint64) { |
| b.popStack() |
| b.popStack() |
| b.instructions = append(b.instructions, asm.Strcpy{ |
| MaxCount: maxCount, |
| }) |
| } |
| |
| // Post posts size bytes from addr to the decoder d. The decoder d must consume |
| // all size bytes before returning; failure to do this will corrupt all |
| // subsequent postbacks. |
| func (b *Builder) Post(addr value.Pointer, size uint64, p Postback) { |
| if !addr.IsValid() { |
| panic(fmt.Errorf("Pointer address %v is not valid", addr)) |
| } |
| b.instructions = append(b.instructions, asm.Post{ |
| Source: addr, |
| Size: size, |
| }) |
| b.decoders = append(b.decoders, p) |
| } |
| |
| // Push pushes val to the top of the stack. |
| func (b *Builder) Push(val value.Value) { |
| if pv, ok := val.(value.Pointer); ok && !pv.IsValid() { |
| panic(fmt.Errorf("PointerValue %v is not valid", val)) |
| } |
| // HACK: RemappedPointers will use the temporary volatileMemoryLayout to |
| // decide the protocol type of the pointer. This will always be |
| // 'unobserved' and therefor a TypeAbsolutePointer instead of a |
| // TypeVolatilePointer. Nothing really cares at the moment though. |
| ty, _ := val.Get(volatileMemoryLayout{}) |
| b.pushStack(ty) |
| b.instructions = append(b.instructions, asm.Push{ |
| Value: val, |
| }) |
| } |
| |
| // Pop removes the top count values from the top of the stack. |
| func (b *Builder) Pop(count uint32) { |
| for i := uint32(0); i < count; i++ { |
| b.popStack() |
| } |
| b.instructions = append(b.instructions, asm.Pop{ |
| Count: count, |
| }) |
| } |
| |
| // MapMemory adds rng as a memory range that needs allocating for replay. |
| func (b *Builder) MapMemory(rng memory.Range) { |
| interval.Merge(&b.mappedMemory, rng.Span(), true) |
| } |
| |
| // Write fills the memory range in capture address-space rng with the data |
| // of resourceID. |
| func (b *Builder) Write(rng memory.Range, resourceID binary.ID) { |
| if rng.Size > 0 { |
| idx, found := b.resourceIDToIdx[resourceID] |
| if !found { |
| idx = uint32(len(b.resources)) |
| b.resourceIDToIdx[resourceID] = idx |
| b.resources = append(b.resources, protocol.ResourceInfo{ |
| ID: resourceID.String(), |
| Size: uint32(rng.Size), |
| }) |
| } |
| b.instructions = append(b.instructions, asm.Resource{ |
| Index: idx, |
| Destination: rng.Base, |
| }) |
| } |
| b.MapMemory(rng) |
| } |
| |
| // Build compiles the replay instructions, returning a Payload that can be |
| // sent to the replay virtual-machine and a ResponseDecoder for interpreting |
| // the responses. |
| func (b *Builder) Build(logger log.Logger) (protocol.Payload, ResponseDecoder, error) { |
| logger = log.Enter(logger, "Build") |
| if config.DebugReplayBuilder { |
| log.Infof(logger, "Instruction count: %d", len(b.instructions)) |
| } |
| |
| vml := b.layoutVolatileMemory(logger) |
| |
| byteOrder := b.architecture.ByteOrder |
| |
| opcodes := &bytes.Buffer{} |
| e := flat.Encoder(endian.Writer(opcodes, byteOrder)) |
| id := uint32(0) |
| for _, i := range b.instructions { |
| if label, ok := i.(asm.Label); ok { |
| id = label.Value |
| } |
| if err := i.Encode(vml, e); err != nil { |
| err = fmt.Errorf("Encode %T failed for atom with id %v: %v", i, id, err) |
| return protocol.Payload{}, nil, err |
| } |
| } |
| |
| payload := protocol.Payload{ |
| StackSize: uint32(512), // TODO: Calculate stack size |
| VolatileMemorySize: uint32(vml.size), |
| Constants: b.constantMemory.data, |
| Resources: b.resources, |
| Opcodes: opcodes.Bytes(), |
| } |
| |
| if config.DebugReplayBuilder { |
| log.Infof(logger, "Stack size: 0x%x", payload.StackSize) |
| log.Infof(logger, "Volatile memory size: 0x%x", payload.VolatileMemorySize) |
| log.Infof(logger, "Constant memory size: 0x%x", len(payload.Constants)) |
| log.Infof(logger, "Opcodes size: 0x%x", len(payload.Opcodes)) |
| log.Infof(logger, "Resource count: %d", len(payload.Resources)) |
| } |
| |
| // TODO: check that each Postback consumes its expected number of bytes. |
| responseDecoder := func(r io.Reader, err error) { |
| var d binary.Decoder |
| if r != nil { |
| d = flat.Decoder(endian.Reader(r, byteOrder)) |
| } |
| go func() { |
| for _, p := range b.decoders { |
| err = p(d, err) |
| if err != nil { |
| d = nil |
| } |
| } |
| }() |
| } |
| return payload, responseDecoder, nil |
| } |
| |
| func (b *Builder) layoutVolatileMemory(logger log.Logger) *volatileMemoryLayout { |
| // Volatile memory layout: |
| // |
| // low ┌──────────────────┐ |
| // │ heap │ |
| // ├──────────────────┤ |
| // │ temp │ |
| // ├──────────────────┤ |
| // │ remapped range 0 │ |
| // ├──────────────────┤ |
| // ├──────────────────┤ |
| // │ remapped range N │ |
| // high └──────────────────┘ |
| |
| tempBase := b.heap.size |
| remapBase := tempBase + b.temp.size |
| |
| remapped := allocator{alignment: b.heap.alignment} |
| remappedBases := make([]uint64, len(b.mappedMemory)) |
| for i, m := range b.mappedMemory { |
| remappedBases[i] = remapBase + remapped.alloc(m.Size) |
| } |
| |
| size := remapBase + remapped.size |
| vml := &volatileMemoryLayout{ |
| mappedMemory: b.mappedMemory, |
| tempBase: tempBase, |
| remappedBases: remappedBases, |
| size: size, |
| } |
| |
| if config.DebugReplayBuilder { |
| log.Infof(logger, "Volatile memory layout: [0x%x, 0x%x]", 0, size-1) |
| log.Infof(logger, " Heap: [0x%x, 0x%x]", 0, tempBase-1) |
| log.Infof(logger, " Temporary: [0x%x, 0x%x]", tempBase, remapBase-1) |
| log.Infof(logger, " Remapped: [0x%x, 0x%x]", remapBase, size-1) |
| for _, m := range b.mappedMemory { |
| log.Infof(logger, " Block: %v", m) |
| } |
| } |
| |
| return vml |
| } |
| |
| type volatileMemoryLayout struct { |
| mappedMemory memory.RangeList // Mapped memory ranges. |
| tempBase uint64 // Base address of the temp space. |
| remappedBases []uint64 // Base address for each remapped entry in mappedMemory. |
| size uint64 // Total size of volatile memory. |
| } |
| |
| // TranslateTemporaryPointer implements the PointerResolver interface method in |
| // the replay/value package. |
| func (l volatileMemoryLayout) TranslateTemporaryPointer(offset uint64) uint64 { |
| return l.tempBase + offset |
| } |
| |
| // TranslateRemappedPointer implements the PointerResolver interface method in |
| // the replay/value package. |
| func (l volatileMemoryLayout) TranslateRemappedPointer(offset uint64) (protocol.Type, uint64) { |
| bufferIdx := interval.IndexOf(&l.mappedMemory, offset) |
| if bufferIdx < 0 { |
| // Pointer is not observed. This can be legal - for example |
| // glVertexAttribPointer may have been passed a pointer that was never |
| // observed. In this situation we pass a pointer that should cause an access |
| // violation if it is dereferenced. We opt to not use 0x00 as this is often |
| // overloaded to mean something else. |
| // Must match value used in cc/gapir/memory_manager.h |
| return protocol.TypeAbsolutePointer, 0xBADF00D |
| } |
| bufferStart := l.mappedMemory[bufferIdx].First() |
| pointer := l.remappedBases[bufferIdx] + offset - uint64(bufferStart) |
| return protocol.TypeVolatilePointer, pointer |
| } |