diff --git a/examples/minicap/README.md b/examples/minicap/README.md index a55596270..5679735ce 100644 --- a/examples/minicap/README.md +++ b/examples/minicap/README.md @@ -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 @@ -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. @@ -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. diff --git a/examples/minicap/main.go b/examples/minicap/main.go index 40242e9de..83005850b 100644 --- a/examples/minicap/main.go +++ b/examples/minicap/main.go @@ -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 ( diff --git a/gralloc/capture_rgba.go b/gralloc/capture_rgba.go index e88df955a..af1e3ebe8 100644 --- a/gralloc/capture_rgba.go +++ b/gralloc/capture_rgba.go @@ -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) } diff --git a/gralloc/fallback_test.go b/gralloc/fallback_test.go new file mode 100644 index 000000000..83568214d --- /dev/null +++ b/gralloc/fallback_test.go @@ -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) +} diff --git a/gralloc/hidlalloc/hidlalloc.go b/gralloc/hidlalloc/hidlalloc.go index 0dc697817..18313dfb1 100644 --- a/gralloc/hidlalloc/hidlalloc.go +++ b/gralloc/hidlalloc/hidlalloc.go @@ -68,7 +68,8 @@ type AllocateResult struct { Ints []int32 } -// Allocate calls IAllocator::allocate() via HIDL/hwbinder. +// Allocate calls IAllocator::allocate() via HIDL/hwbinder. It supports one +// buffer per call because AllocateResult represents a single native handle. func Allocate( ctx context.Context, transport binder.Transport, @@ -85,10 +86,31 @@ func Allocate( // BufferDescriptor: goldfish/ranchu encoding (5 uint32 values). descriptor := []uint32{width, height, layerCount, format, uint32(usage)} + return AllocateDescriptor(ctx, transport, allocatorHandle, descriptor, count) +} + +// AllocateDescriptor calls IAllocator::allocate with a descriptor produced +// using the target mapper's descriptor ABI. It supports one buffer per call. +func AllocateDescriptor( + ctx context.Context, + transport binder.Transport, + allocatorHandle uint32, + descriptor []uint32, + count uint32, +) (_ *AllocateResult, _err error) { + if len(descriptor) == 0 { + return nil, fmt.Errorf("hidlalloc: empty buffer descriptor") + } + if count == 0 { + return nil, fmt.Errorf("hidlalloc: buffer count must be positive") + } + if count != 1 { + return nil, fmt.Errorf("hidlalloc: only one buffer per allocation is supported (count=%d)", count) + } hp := hwparcel.New() hp.WriteInterfaceToken(allocatorFQName) - hp.WriteHidlVecUint32(descriptor) + hp.WriteHidlVecUint32(append([]uint32(nil), descriptor...)) hp.WriteUint32(count) reply, err := hwservicemanager.TransactHidl(ctx, transport, allocatorHandle, transactionAllocate, hp) @@ -109,8 +131,8 @@ func Allocate( // // Wire format after scatter-gather resolution: // -// [0:4] int32 error -// [4:8] padding (alignment) +// [0:4] int32 HIDL transport status +// [4:8] int32 allocator error // [8:12] int32 stride // [12:52] binder_buffer_object: hidl_vec header (bufLen=16) // [52:92] binder_buffer_object: hidl_handle (bufLen=16, child of vec) @@ -131,8 +153,12 @@ func parseAllocateResponse(p *parcel.Parcel) (*AllocateResult, error) { return nil, fmt.Errorf("response too short: %d bytes", len(data)) } + transportStatus := int32(binary.LittleEndian.Uint32(data[0:])) + if transportStatus != 0 { + return nil, fmt.Errorf("HIDL allocator transport status: %d", transportStatus) + } result := &AllocateResult{ - Error: int32(binary.LittleEndian.Uint32(data[0:])), + Error: int32(binary.LittleEndian.Uint32(data[4:])), Stride: int32(binary.LittleEndian.Uint32(data[8:])), } diff --git a/gralloc/hidlalloc/hidlalloc_test.go b/gralloc/hidlalloc/hidlalloc_test.go index d8ea5e7e0..a3fb47eed 100644 --- a/gralloc/hidlalloc/hidlalloc_test.go +++ b/gralloc/hidlalloc/hidlalloc_test.go @@ -68,3 +68,50 @@ func TestAllocateClosesTransferredDescriptorsOnFailure(t *testing.T) { _, err = unix.FcntlInt(uintptr(fds[1]), unix.F_GETFD, 0) require.NoError(t, err) } + +func TestParseAllocateResponseReadsAllocatorError(t *testing.T) { + raw := make([]byte, 12) + binary.LittleEndian.PutUint32(raw[4:], 1) + binary.LittleEndian.PutUint32(raw[8:], 64) + + result, err := parseAllocateResponse(parcel.FromReceivedBytes(raw, nil)) + require.NoError(t, err) + require.Equal(t, int32(1), result.Error) + require.Equal(t, int32(64), result.Stride) +} + +func TestParseAllocateResponseRejectsTransportError(t *testing.T) { + raw := make([]byte, 12) + binary.LittleEndian.PutUint32(raw, 1) + + _, err := parseAllocateResponse(parcel.FromReceivedBytes(raw, nil)) + require.ErrorContains(t, err, "HIDL allocator transport status: 1") +} + +func TestAllocateDescriptorValidatesRequest(t *testing.T) { + _, err := AllocateDescriptor(context.Background(), nil, 0, nil, 1) + require.ErrorContains(t, err, "empty buffer descriptor") + + _, err = AllocateDescriptor(context.Background(), nil, 0, []uint32{1}, 0) + require.ErrorContains(t, err, "buffer count must be positive") +} + +func TestAllocateRejectsMultipleBuffers(t *testing.T) { + for _, allocate := range []struct { + name string + call func() (*AllocateResult, error) + }{ + {"descriptor", func() (*AllocateResult, error) { + return AllocateDescriptor(context.Background(), allocationTransport{reply: parcel.FromReceivedBytes(make([]byte, 12), nil)}, 0, []uint32{1}, 2) + }}, + {"legacy", func() (*AllocateResult, error) { + return Allocate(context.Background(), allocationTransport{reply: parcel.FromReceivedBytes(make([]byte, 12), nil)}, 0, 1, 1, 1, 1, 1, 2) + }}, + } { + t.Run(allocate.name, func(t *testing.T) { + result, err := allocate.call() + require.ErrorContains(t, err, "only one buffer") + require.Nil(t, result) + }) + } +} diff --git a/gralloc/mali_allocate.go b/gralloc/mali_allocate.go new file mode 100644 index 000000000..8e1815e25 --- /dev/null +++ b/gralloc/mali_allocate.go @@ -0,0 +1,150 @@ +package gralloc + +import ( + "context" + "errors" + "fmt" + "math" + + "github.com/AndroidGoLab/binder/android/hardware/common" + gfx "github.com/AndroidGoLab/binder/android/hardware/graphics/common" + "github.com/AndroidGoLab/binder/binder" + "github.com/AndroidGoLab/binder/gralloc/hidlalloc" + "golang.org/x/sys/unix" +) + +const ( + maliDescriptorVersion = 30 + maliHandleMagic = 0x03141592 + maliHandleIntCount = 129 + maliRGBAUsage = uint64(gfx.BufferUsageCpuReadOften | gfx.BufferUsageGpuTexture | gfx.BufferUsageGpuRenderTarget | gfx.BufferUsageComposerOverlay) + + maliWidthIndex = 2 + maliHeightIndex = 3 + maliFormatIndex = 4 + maliProducerUsageIndex = 85 + maliConsumerUsageIndex = 87 + maliStrideIndex = 89 + maliInternalFormatIndex = 91 + maliPlaneOffsetIndex = 93 + maliPlaneByteStrideIndex = 94 + maliPlaneAllocWidthIndex = 95 + maliPlaneAllocHeightIndex = 96 +) + +func maliDescriptor(width, height int32) ([]uint32, error) { + if width <= 0 || height <= 0 { + return nil, errors.New("buffer dimensions must be positive") + } + return []uint32{ + maliDescriptorVersion, + uint32(width), + uint32(height), + 1, + uint32(gfx.PixelFormatRgba8888), + uint32(maliRGBAUsage), + uint32(maliRGBAUsage >> 32), + 0, + 0, + }, nil +} + +func allocateMaliRGBA(ctx context.Context, driver binder.Transport, handle uint32, width, height int32, count int) (_ []*Buffer, resultErr error) { + if count < 1 || count > 64 { + return nil, errors.New("invalid Mali buffer count") + } + desc, err := maliDescriptor(width, height) + if err != nil { + return nil, err + } + var buffers []*Buffer + defer func() { + if resultErr != nil { + resultErr = errors.Join(resultErr, CloseBuffers(buffers)) + } + }() + + for range count { + result, err := hidlalloc.AllocateDescriptor(ctx, driver, handle, desc, 1) + if err != nil { + return nil, fmt.Errorf("allocate Mali buffer: %w", err) + } + if result.Error != 0 { + return nil, fmt.Errorf("Mali allocator returned error %d", result.Error) + } + buffer := &Buffer{ + Handle: common.NativeHandle{Fds: result.Fds, Ints: result.Ints}, + Stride: result.Stride, + Width: uint32(width), + Height: uint32(height), + Format: int32(gfx.PixelFormatRgba8888), + Usage: maliRGBAUsage, + } + buffers = append(buffers, buffer) + if _, err := maliRGBALayout(buffer); err != nil { + return nil, fmt.Errorf("unsupported Mali allocation: %w", err) + } + } + return buffers, nil +} + +func maliRGBALayout(b *Buffer) (rgbaLayout, error) { + var out rgbaLayout + if b == nil || len(b.Handle.Fds) != 2 || + b.Handle.Fds[0] < 0 || + b.Handle.Fds[1] < 0 || + len(b.Handle.Ints) != maliHandleIntCount { + return out, errors.New("unsupported Mali handle shape") + } + a := b.Handle.Ints + if a[0] != maliHandleMagic { + return out, errors.New("unsupported Mali handle magic") + } + if b.Width == 0 || b.Height == 0 || + uint32(a[maliWidthIndex]) != b.Width || + uint32(a[maliHeightIndex]) != b.Height || + a[maliFormatIndex] != int32(gfx.PixelFormatRgba8888) || + b.Format != int32(gfx.PixelFormatRgba8888) { + return out, errors.New("inconsistent Mali buffer dimensions or format") + } + producerUsage := uint64(uint32(a[maliProducerUsageIndex])) | uint64(uint32(a[maliProducerUsageIndex+1]))<<32 + consumerUsage := uint64(uint32(a[maliConsumerUsageIndex])) | uint64(uint32(a[maliConsumerUsageIndex+1]))<<32 + if producerUsage != maliRGBAUsage || consumerUsage != maliRGBAUsage || b.Usage != maliRGBAUsage { + return out, errors.New("unsupported Mali buffer usage") + } + if uint64(uint32(a[maliInternalFormatIndex]))|uint64(uint32(a[maliInternalFormatIndex+1]))<<32 != uint64(gfx.PixelFormatRgba8888) { + return out, errors.New("unsupported Mali internal format or compression") + } + stridePixels := uint64(uint32(a[maliStrideIndex])) | uint64(uint32(a[maliStrideIndex+1]))<<32 + byteStride := uint64(uint32(a[maliPlaneByteStrideIndex])) + allocWidth := uint64(uint32(a[maliPlaneAllocWidthIndex])) + allocHeight := uint64(uint32(a[maliPlaneAllocHeightIndex])) + offset := uint64(uint32(a[maliPlaneOffsetIndex])) + if a[maliPlaneByteStrideIndex] <= 0 || + a[maliPlaneAllocWidthIndex] <= 0 || + a[maliPlaneAllocHeightIndex] <= 0 || + stridePixels < uint64(b.Width) || + stridePixels > math.MaxInt/rgbaBytesPerPixel || + byteStride != stridePixels*rgbaBytesPerPixel || + allocWidth < uint64(b.Width) || + allocHeight < uint64(b.Height) || + offset != 0 || + b.Stride <= 0 || + uint64(b.Stride) != stridePixels { + return out, errors.New("invalid Mali RGBA plane layout") + } + stride := int(byteStride) + if uint64(b.Height) > (math.MaxInt-offset)/uint64(stride) { + return out, errors.New("Mali buffer dimensions overflow") + } + length := int(offset + uint64(stride)*uint64(b.Height)) + var stat unix.Stat_t + if err := unix.Fstat(int(b.Handle.Fds[0]), &stat); err != nil { + return out, fmt.Errorf("stat Mali buffer: %w", err) + } + if stat.Size < int64(length) { + return out, fmt.Errorf("Mali pixel plane exceeds dma-buf size: need %d, have %d", length, stat.Size) + } + out = rgbaLayout{fd: int(b.Handle.Fds[0]), offset: int(offset), stride: stride, length: length} + return out, nil +} diff --git a/gralloc/mali_allocate_test.go b/gralloc/mali_allocate_test.go new file mode 100644 index 000000000..9a78b64fe --- /dev/null +++ b/gralloc/mali_allocate_test.go @@ -0,0 +1,76 @@ +package gralloc + +import ( + "testing" + + "github.com/AndroidGoLab/binder/android/hardware/common" + gfx "github.com/AndroidGoLab/binder/android/hardware/graphics/common" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" +) + +func TestMaliDescriptor(t *testing.T) { + descriptor, err := maliDescriptor(720, 1280) + require.NoError(t, err) + require.Equal(t, []uint32{30, 720, 1280, 1, 1, 0xb03, 0, 0, 0}, descriptor) + + _, err = maliDescriptor(0, 1280) + require.Error(t, err) +} + +func TestMaliRGBALayout(t *testing.T) { + fd, err := unix.MemfdCreate("mali-rgba-test", 0) + require.NoError(t, err) + defer unix.Close(fd) + require.NoError(t, unix.Ftruncate(fd, 24)) + attrFD, err := unix.Dup(fd) + require.NoError(t, err) + defer unix.Close(attrFD) + + ints := make([]int32, maliHandleIntCount) + ints[0] = maliHandleMagic + ints[maliWidthIndex] = 2 + ints[maliHeightIndex] = 2 + ints[maliFormatIndex] = int32(gfx.PixelFormatRgba8888) + ints[maliProducerUsageIndex] = int32(maliRGBAUsage) + ints[maliConsumerUsageIndex] = int32(maliRGBAUsage) + ints[maliStrideIndex] = 3 + ints[maliInternalFormatIndex] = int32(gfx.PixelFormatRgba8888) + ints[maliPlaneByteStrideIndex] = 12 + ints[maliPlaneAllocWidthIndex] = 3 + ints[maliPlaneAllocHeightIndex] = 2 + + buffer := &Buffer{ + Handle: common.NativeHandle{Fds: []int32{int32(fd), int32(attrFD)}, Ints: ints}, + Stride: 3, + Width: 2, + Height: 2, + Format: int32(gfx.PixelFormatRgba8888), + Usage: maliRGBAUsage, + } + layout, err := maliRGBALayout(buffer) + require.NoError(t, err) + require.Equal(t, rgbaLayout{fd: fd, stride: 12, length: 24}, layout) + captureLayout, err := captureRGBALayout(buffer) + require.NoError(t, err) + require.Equal(t, layout, captureLayout) + + source := []byte{1, 2, 3, 4, 5, 6, 7, 8, 99, 99, 99, 99, 9, 10, 11, 12, 13, 14, 15, 16, 99, 99, 99, 99} + destination := make([]byte, 16) + copyRGBARows(destination, source, layout, buffer.Width, buffer.Height) + require.Equal(t, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, destination) + + buffer.Handle.Ints[maliInternalFormatIndex] = 0x101 + _, err = maliRGBALayout(buffer) + require.Error(t, err) + + buffer.Handle.Ints[maliInternalFormatIndex] = int32(gfx.PixelFormatRgba8888) + buffer.Handle.Ints[maliPlaneOffsetIndex] = 1 + _, err = maliRGBALayout(buffer) + require.Error(t, err) + + buffer.Handle.Ints[maliPlaneOffsetIndex] = 0 + buffer.Handle.Fds[1] = -1 + _, err = maliRGBALayout(buffer) + require.Error(t, err) +} diff --git a/gralloc/qcom_allocate.go b/gralloc/qcom_allocate.go index 869f4e6d9..0ba36bfbe 100644 --- a/gralloc/qcom_allocate.go +++ b/gralloc/qcom_allocate.go @@ -41,6 +41,12 @@ const ( qcomIonFlag int32 = 8 ) +// qcomAllocatorFQName identifies the allocator used by the Qualcomm profile. +const qcomAllocatorFQName = "android.hardware.graphics.allocator@2.0::IAllocator" + +// maliAllocatorFQName identifies the allocator used by the Mali profile. +const maliAllocatorFQName = "android.hardware.graphics.allocator@3.0::IAllocator" + // qcomDescriptor serializes the Android 10 SDM845 gralloc2 buffer descriptor. // https://android.googlesource.com/platform/hardware/qcom/sdm845/display/+/refs/tags/android-10.0.0_r40/gralloc/gr_buf_descriptor.h // @@ -161,7 +167,17 @@ func readQcomAllocation(p *parcel.Parcel) (_ int32, _ common.NativeHandle, resul func AllocateRGBA(ctx context.Context, sm *servicemanager.ServiceManager, api int, width, height int32, count int) ([]*Buffer, error) { switch api { case 29: - return allocateQcomRGBA(ctx, width, height, count) + if count < 1 || count > 64 { + return nil, errors.New("invalid Qualcomm buffer count") + } + if _, err := qcomDescriptor(width, height); err != nil { + return nil, err + } + driver, err := kernelbinder.Open(ctx, binder.WithDevicePath("/dev/hwbinder"), binder.WithMapSize(256*1024)) + if err != nil { + return nil, fmt.Errorf("open hwbinder: %w", err) + } + return allocateAndroid10RGBA(ctx, driver, width, height, count) case 34: return AllocatePixelRGBA(ctx, sm, api, width, height, count) default: @@ -169,10 +185,39 @@ func AllocateRGBA(ctx context.Context, sm *servicemanager.ServiceManager, api in } } +// allocateAndroid10RGBA probes Android 10 allocators and owns the connection. +// +// Example: an absent Qualcomm allocator triggers the Mali probe. +func allocateAndroid10RGBA(ctx context.Context, driver binder.Transport, width, height int32, count int) (buffers []*Buffer, resultErr error) { + defer func() { + resultErr = errors.Join(resultErr, driver.Close(context.Background())) + if resultErr != nil { + resultErr = errors.Join(resultErr, CloseBuffers(buffers)) + buffers = nil + } + }() + sm := hwservicemanager.New(driver) + handle, err := sm.GetService(ctx, qcomAllocatorFQName, "default") + if err != nil { + return nil, fmt.Errorf("get allocator@2.0: %w", err) + } + if handle != 0 { + return allocateQcomRGBA(ctx, driver, handle, width, height, count) + } + handle, err = sm.GetService(ctx, maliAllocatorFQName, "default") + if err != nil { + return nil, fmt.Errorf("get allocator@3.0: %w", err) + } + if handle == 0 { + return nil, errors.New("allocator@3.0 unavailable") + } + return allocateMaliRGBA(ctx, driver, handle, width, height, count) +} + // allocateQcomRGBA uses Android 10's HIDL allocator@2 and validates each handle. // // Example: allocate one buffer per transaction to simplify HIDL FD ownership. -func allocateQcomRGBA(ctx context.Context, width, height int32, count int) (_ []*Buffer, resultErr error) { +func allocateQcomRGBA(ctx context.Context, driver binder.Transport, handle uint32, width, height int32, count int) (_ []*Buffer, resultErr error) { if count < 1 || count > 64 { return nil, errors.New("invalid Qualcomm buffer count") } @@ -180,28 +225,18 @@ func allocateQcomRGBA(ctx context.Context, width, height int32, count int) (_ [] if err != nil { return nil, err } - driver, err := kernelbinder.Open(ctx, binder.WithDevicePath("/dev/hwbinder"), binder.WithMapSize(256*1024)) - if err != nil { - return nil, fmt.Errorf("open hwbinder: %w", err) - } var buffers []*Buffer - defer // Close partial buffers and the HIDL connection on error. + defer // Close partial buffers on error. // // Example: an unsupported second handle releases the first allocation. func() { - resultErr = errors.Join(resultErr, driver.Close(context.Background())) if resultErr != nil { resultErr = errors.Join(resultErr, CloseBuffers(buffers)) } }() - const fqName = "android.hardware.graphics.allocator@2.0::IAllocator" - handle, err := hwservicemanager.New(driver).GetService(ctx, fqName, "default") - if err != nil { - return nil, fmt.Errorf("get Qualcomm allocator@2: %w", err) - } for range count { req := hwparcel.New() - req.WriteInterfaceToken(fqName) + req.WriteInterfaceToken(qcomAllocatorFQName) req.WriteHidlVecUint32(desc) req.WriteUint32(1) reply, err := hwservicemanager.TransactHidl(ctx, driver, handle, 2, req)