Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions examples/minicap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

This example captures a virtual display and serves minicap-compatible JPEG
frames on an Android abstract Unix socket. It also supports raw RGBA output.
Running it requires access to SurfaceFlinger and the graphics allocator; the
examples below use a privileged account.
Running it requires access to SurfaceFlinger and the graphics allocator. Some
devices grant this access to the ADB shell; others require a privileged account.

```sh
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o minicap ./examples/minicap
adb push minicap /data/local/tmp/minicap
adb shell su 0 /data/local/tmp/minicap -P 1080x2400@360x800/0 -n minicap
adb shell /data/local/tmp/minicap -P 1080x2400@360x800/0 -n minicap
```

Use the physical display dimensions for the first pair in `-P` and the maximum
Expand All @@ -20,6 +20,14 @@ JPEG screenshot to stdout (`-s`). The default display is 0. Secondary-display
capture requires an available layer-stack mapping; Android 10 does not expose
that mapping through the supported native interface.

For stdout modes invoked through `adb exec-out`, redirect the remote command's
stderr so the startup PID message is not mixed with the output:

```sh
adb exec-out sh -c '/data/local/tmp/minicap -P 1080x2400@360x800/0 -t 2>/dev/null'
adb exec-out sh -c '/data/local/tmp/minicap -P 1080x2400@360x800/0 -s 2>/dev/null' > screenshot.jpg
```

On Android 10, the virtual display requests secure composition, matching native
minicap; Android 14 uses a non-secure virtual display. Secure layers may be
omitted or blacked out, and protected graphics buffers are unsupported.
Expand All @@ -37,20 +45,30 @@ quarter turns and a quirks byte. Subsequent frames contain a little-endian
32-bit JPEG byte length followed by that many JPEG bytes. The server accepts a
new client after the previous client disconnects.

Frames are delivered when the display producer queues updated buffers; this is
not a per-view change event, and a static screen may not produce new frames.
`-r` caps the output rate, while `-S` skips older queued frames in favor of the
latest available frame.

For unframed, tightly packed RGBA8888 frames on stdout, use `-raw`:

```sh
adb exec-out su 0 /data/local/tmp/minicap -P 1080x2400@360x800/0 -raw > frames.rgba
adb exec-out sh -c '/data/local/tmp/minicap -P 1080x2400@360x800/0 -raw 2>/dev/null' > frames.rgba
```

Each raw frame is `output_width * output_height * 4` bytes. For 90- or
270-degree projection, the output width and height are exchanged. Terminate
capture with SIGINT or SIGTERM.

The graphics backends recognize the Android 14 r35 Pixel gchips layout and
the Android 10 Qualcomm SDM845 uncached, linear ION layout. Unknown layouts
fail rather than being interpreted as pixels. Android 14 capture has been
exercised on a Pixel 6a; the Android 10 protocol and allocator have
source-based tests but have not been verified on Android 10 hardware. Other
Android versions and allocator layouts require additional support; Android 10
on arm64 alone does not establish that the SDM845 backend applies.
capture with SIGINT or SIGTERM. Since raw output has no framing, read fixed-size
frame chunks and discard any incomplete trailing bytes if capture is interrupted
mid-frame.

The graphics backends recognize the Android 14 r35 Pixel gchips layout, the
Android 10 Qualcomm SDM845 uncached, linear ION layout, and the tested Rockchip
Bifrost gralloc3 ABI (allocator@3.0 descriptor version 30 with a linear,
CPU-readable RGBA dma-buf). The Rockchip handle reader validates the observed
native-handle shape and plane geometry; unknown or compressed layouts fail
rather than being interpreted as pixels. This confirms support for the tested
device's ABI, not every RK3588 board or Mali allocator. Android 14 capture has
been exercised on a Pixel 6a; Android 10 Qualcomm remains source-tested but
has not been verified on SDM845 hardware. Android 10 on arm64 alone does not
establish that the Qualcomm backend applies.
5 changes: 2 additions & 3 deletions examples/minicap/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
// On a supported device, run as a user with SurfaceFlinger capture permission:
// minicap -P 1080x2400@360x800/0 -n minicap
// Use -raw to write tightly packed RGBA frames to stdout. Android 14 Pixel
// gchips and Android 10 Qualcomm SDM845 are the supported allocator layouts.
// The Android 10 path has source-based tests but has not been tested on a device;
// unsupported handles are rejected.
// gchips, Android 10 Qualcomm SDM845, and the tested Rockchip Bifrost
// allocator@3.0 handle layout are supported; unknown layouts are rejected.
package main

import (
Expand Down
3 changes: 3 additions & 0 deletions gralloc/capture_rgba.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ func captureRGBALayout(b *Buffer) (rgbaLayout, error) {
if b != nil && len(b.Handle.Ints) == 22 && b.Handle.Ints[0] == qcomHandleMagic {
return qcomRGBALayout(b)
}
if b != nil && len(b.Handle.Ints) == maliHandleIntCount && b.Handle.Ints[0] == maliHandleMagic {
return maliRGBALayout(b)
}
return pixelRGBALayout(b)
}

Expand Down
262 changes: 262 additions & 0 deletions gralloc/fallback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
package gralloc

import (
"bytes"
"context"
"encoding/binary"
"errors"
"testing"

"github.com/AndroidGoLab/binder/binder"
"github.com/AndroidGoLab/binder/parcel"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)

// allocatorScript simulates hwservicemanager and allocator transactions.
//
// Example: return a null Qualcomm service followed by a Mali allocation.
type allocatorScript struct {
binder.Transport
replies []*parcel.Parcel
failure error
closeErr error
calls int
closes int
requests [][]byte
handles []uint32
codes []binder.TransactionCode
}

// Transact returns the next scripted response.
//
// Example: simulate an unavailable allocator and an available fallback.
func (s *allocatorScript) Transact(_ context.Context, handle uint32, code binder.TransactionCode, _ binder.TransactionFlags, data *parcel.Parcel) (*parcel.Parcel, error) {
s.requests = append(s.requests, append([]byte(nil), data.Data()...))
s.handles = append(s.handles, handle)
s.codes = append(s.codes, code)
s.calls++
if len(s.replies) == 0 {
return nil, s.failure
}
reply := s.replies[0]
s.replies = s.replies[1:]
return reply, nil
}

// Close simulates releasing the hwbinder connection.
//
// Example: report an error closing a connection after successful allocation.
func (s *allocatorScript) Close(context.Context) error {
s.closes++
return s.closeErr
}

// serviceReply constructs a HIDL service lookup response.
//
// Example: a zero handle indicates an unregistered service.
func serviceReply(handle uint32) *parcel.Parcel {
raw := make([]byte, 28)
binary.LittleEndian.PutUint32(raw[4:], 0x73682a85)
binary.LittleEndian.PutUint32(raw[12:], handle)
return parcel.FromReceivedBytes(raw, nil)
}

// maliReply creates a HIDL allocation with owned file descriptors.
//
// Example: corrupt the magic to test rejection and cleanup.
func maliReply(t *testing.T, valid bool) (*parcel.Parcel, []int) {
t.Helper()
fd, err := unix.MemfdCreate("mali-test", 0)
require.NoError(t, err)
require.NoError(t, unix.Ftruncate(fd, 4))
attr, err := unix.Dup(fd)
require.NoError(t, err)
t.Cleanup(func() { _ = unix.Close(fd); _ = unix.Close(attr) })
ints := make([]int32, maliHandleIntCount)
if valid {
ints[0] = maliHandleMagic
}
ints[maliWidthIndex] = 1
ints[maliHeightIndex] = 1
ints[maliFormatIndex] = 1
ints[maliProducerUsageIndex] = int32(maliRGBAUsage)
ints[maliConsumerUsageIndex] = int32(maliRGBAUsage)
ints[maliStrideIndex] = 1
ints[maliInternalFormatIndex] = 1
ints[maliPlaneByteStrideIndex] = 4
ints[maliPlaneAllocWidthIndex] = 1
ints[maliPlaneAllocHeightIndex] = 1
const off = 164
data := make([]byte, off+12+8+4*maliHandleIntCount)
binary.LittleEndian.PutUint32(data[8:], 1)
for _, pos := range []int{12, 52, 92} {
binary.LittleEndian.PutUint32(data[pos:], 0x70742a85)
}
binary.LittleEndian.PutUint64(data[100:], off)
binary.LittleEndian.PutUint64(data[108:], uint64(len(data)-off))
binary.LittleEndian.PutUint32(data[off:], 12)
binary.LittleEndian.PutUint32(data[off+4:], 2)
binary.LittleEndian.PutUint32(data[off+8:], maliHandleIntCount)
binary.LittleEndian.PutUint32(data[off+12:], uint32(fd))
binary.LittleEndian.PutUint32(data[off+16:], uint32(attr))
for i, v := range ints {
binary.LittleEndian.PutUint32(data[off+20+i*4:], uint32(v))
}
return parcel.FromReceivedBytes(data, nil, off+12, off+16), []int{fd, attr}
}

// TestAndroid10Fallback exercises service discovery and real Mali reply handling.
//
// Example: an absent Qualcomm allocator falls back to a registered Mali service.
func TestAndroid10Fallback(t *testing.T) {
reply, fds := maliReply(t, true)
s := &allocatorScript{replies: []*parcel.Parcel{serviceReply(0), serviceReply(7), reply}}
buffers, err := allocateAndroid10RGBA(context.Background(), s, 1, 1, 1)
require.NoError(t, err)
require.Len(t, buffers, 1)
require.Equal(t, 3, s.calls)
require.Equal(t, 1, s.closes)
require.Equal(t, []uint32{0, 0, 7}, s.handles)
require.Equal(t, []binder.TransactionCode{1, 1, 2}, s.codes)
require.True(t, bytes.Contains(s.requests[2], []byte("android.hardware.graphics.allocator@3.0::IAllocator")))
for _, fd := range fds {
_, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
require.NoError(t, err)
}
require.NoError(t, CloseBuffers(buffers))
}

// TestAndroid10FallbackFailureCleanup checks that errors do not leak allocations.
//
// Example: an invalid second allocation closes both its descriptors and the first buffer.
func TestAndroid10FallbackFailureCleanup(t *testing.T) {
first, fds1 := maliReply(t, true)
second, fds2 := maliReply(t, false)
s := &allocatorScript{replies: []*parcel.Parcel{serviceReply(0), serviceReply(7), first, second}}
buffers, err := allocateAndroid10RGBA(context.Background(), s, 1, 1, 2)
require.ErrorContains(t, err, "unsupported Mali allocation")
require.Nil(t, buffers)
require.Equal(t, 4, s.calls)
require.Equal(t, 1, s.closes)
for _, fd := range append(fds1, fds2...) {
_, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
require.ErrorIs(t, err, unix.EBADF)
}
}

// TestAndroid10DoesNotFallbackOnDiscoveryError preserves genuine failures.
//
// Example: a failed Qualcomm service transaction must not query Mali.
func TestAndroid10DoesNotFallbackOnDiscoveryError(t *testing.T) {
failure := errors.New("lookup failure")
s := &allocatorScript{failure: failure}
buffers, err := allocateAndroid10RGBA(context.Background(), s, 1, 1, 1)
require.ErrorIs(t, err, failure)
require.Nil(t, buffers)
require.Equal(t, 1, s.calls)
require.Equal(t, 1, s.closes)
}

// TestAndroid10CloseFailureClosesBuffers protects descriptors on failed return.
//
// Example: a connection close failure after allocation invalidates its buffers.
func TestAndroid10CloseFailureClosesBuffers(t *testing.T) {
reply, fds := maliReply(t, true)
failure := errors.New("close failure")
s := &allocatorScript{replies: []*parcel.Parcel{serviceReply(0), serviceReply(7), reply}, closeErr: failure}
buffers, err := allocateAndroid10RGBA(context.Background(), s, 1, 1, 1)
require.ErrorIs(t, err, failure)
require.Nil(t, buffers)
require.Equal(t, 1, s.closes)
for _, fd := range fds {
_, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0)
require.ErrorIs(t, err, unix.EBADF)
}
}

// TestAndroid10QualcommSuccessStopsDiscovery verifies the preferred allocator.
//
// Example: a valid Qualcomm buffer prevents a Mali lookup.
func TestAndroid10QualcommSuccessStopsDiscovery(t *testing.T) {
fd, err := unix.MemfdCreate("qcom-test", 0)
require.NoError(t, err)
attr, err := unix.Dup(fd)
require.NoError(t, err)
t.Cleanup(func() { _ = unix.Close(fd); _ = unix.Close(attr) })
raw := make([]byte, 308)
binary.LittleEndian.PutUint32(raw[8:], 1)
for _, off := range []int{12, 52, 92} {
binary.LittleEndian.PutUint32(raw[off:], 0x70742a85)
}
binary.LittleEndian.PutUint64(raw[20:], 164)
binary.LittleEndian.PutUint64(raw[28:], 16)
binary.LittleEndian.PutUint32(raw[172:], 1)
binary.LittleEndian.PutUint64(raw[100:], 200)
binary.LittleEndian.PutUint64(raw[108:], 108)
binary.LittleEndian.PutUint32(raw[132:], 0x66646185)
binary.LittleEndian.PutUint64(raw[140:], 2)
binary.LittleEndian.PutUint64(raw[148:], 2)
binary.LittleEndian.PutUint64(raw[156:], 12)
binary.LittleEndian.PutUint32(raw[200:], 12)
binary.LittleEndian.PutUint32(raw[204:], 2)
binary.LittleEndian.PutUint32(raw[208:], 22)
binary.LittleEndian.PutUint32(raw[212:], uint32(fd))
binary.LittleEndian.PutUint32(raw[216:], uint32(attr))
ints := []int32{qcomHandleMagic, qcomIonFlag, 1, 1, 1, 1, 1, 0, 1, 0, 0, int32(qcomRGBAUsage), 0, 4}
for i, v := range ints {
binary.LittleEndian.PutUint32(raw[220+i*4:], uint32(v))
}
reply := parcel.FromReceivedBytes(raw, []uint64{12, 52, 92, 132}, 212, 216)
s := &allocatorScript{replies: []*parcel.Parcel{serviceReply(9), reply}}
buffers, err := allocateAndroid10RGBA(context.Background(), s, 1, 1, 1)
require.NoError(t, err)
require.Len(t, buffers, 1)
require.Equal(t, 1, s.closes)
require.Equal(t, []uint32{0, 9}, s.handles)
require.Equal(t, []binder.TransactionCode{1, 2}, s.codes)
require.True(t, bytes.Contains(s.requests[1], []byte("android.hardware.graphics.allocator@2.0::IAllocator")))
require.NoError(t, CloseBuffers(buffers))
}

// TestAndroid10MissingServicesAndCloseFailure reports both failures.
//
// Example: a failed connection close remains visible after both probes fail.
func TestAndroid10MissingServicesAndCloseFailure(t *testing.T) {
failure := errors.New("close failure")
s := &allocatorScript{replies: []*parcel.Parcel{serviceReply(0), serviceReply(0)}, closeErr: failure}
buffers, err := allocateAndroid10RGBA(context.Background(), s, 1, 1, 1)
require.ErrorContains(t, err, "allocator@3.0 unavailable")
require.ErrorIs(t, err, failure)
require.Nil(t, buffers)
require.Equal(t, 2, s.calls)
require.Equal(t, 1, s.closes)
}

// TestAndroid10SecondLookupFailureClosesConnection checks failed Mali discovery.
//
// Example: an unavailable allocator@2 followed by a failed allocator@3 lookup.
func TestAndroid10SecondLookupFailureClosesConnection(t *testing.T) {
failure := errors.New("Mali lookup failure")
s := &allocatorScript{replies: []*parcel.Parcel{serviceReply(0)}, failure: failure}
buffers, err := allocateAndroid10RGBA(context.Background(), s, 1, 1, 1)
require.ErrorIs(t, err, failure)
require.Nil(t, buffers)
require.Equal(t, []uint32{0, 0}, s.handles)
require.Equal(t, 1, s.closes)
}

// TestAndroid10AllocationErrorDoesNotProbeAnotherBackend checks HAL errors.
//
// Example: an allocator@2 error does not cause an allocator@3 lookup.
func TestAndroid10AllocationErrorDoesNotProbeAnotherBackend(t *testing.T) {
raw := make([]byte, 12)
binary.LittleEndian.PutUint32(raw[4:], 1)
s := &allocatorScript{replies: []*parcel.Parcel{serviceReply(9), parcel.FromReceivedBytes(raw, nil)}}
buffers, err := allocateAndroid10RGBA(context.Background(), s, 1, 1, 1)
require.ErrorContains(t, err, "HIDL allocator status=0 error=1")
require.Nil(t, buffers)
require.Equal(t, []uint32{0, 9}, s.handles)
require.Equal(t, []binder.TransactionCode{1, 2}, s.codes)
require.Equal(t, 1, s.closes)
}
Loading
Loading