333 lines
10 KiB
Go
333 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/mil-ad/wlthumbs/proto"
|
|
"github.com/rajveermalviya/go-wayland/wayland/client"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
type toplevel struct {
|
|
handle *proto.ExtForeignToplevelHandle
|
|
id, title, appID string
|
|
closed, done bool
|
|
}
|
|
|
|
type output struct {
|
|
wl *client.Output
|
|
name string
|
|
make_, model string
|
|
done bool
|
|
}
|
|
|
|
// source is a normalized capture target (window or monitor). captureOne fills
|
|
// the trailing fields with the captured frame.
|
|
type source struct {
|
|
typ string // "window" | "output"
|
|
id string
|
|
app string
|
|
title string
|
|
cap *proto.ExtImageCaptureSource
|
|
|
|
raw []byte // captured pixels (nil until/unless capture succeeds)
|
|
w, h uint32
|
|
transform uint32
|
|
swapRB bool
|
|
thumb string // path to the written PNG (empty if capture/encode failed)
|
|
}
|
|
|
|
// app holds the Wayland connection and the globals/objects we bind on it.
|
|
type app struct {
|
|
display *client.Display
|
|
registry *client.Registry
|
|
shm *client.Shm
|
|
copyMgr *proto.ExtImageCopyCaptureManager
|
|
toplevelSrcMgr *proto.ExtForeignToplevelImageCaptureSourceManager
|
|
outputSrcMgr *proto.ExtOutputImageCaptureSourceManager
|
|
toplevelList *proto.ExtForeignToplevelList
|
|
|
|
toplevels []*toplevel
|
|
outputs []*output
|
|
}
|
|
|
|
// connect opens the Wayland connection, binds the globals we need, and verifies
|
|
// the compositor supports the required capture protocols.
|
|
func (a *app) connect() error {
|
|
display, err := client.Connect("")
|
|
if err != nil {
|
|
return fmt.Errorf("connect to wayland: %w", err)
|
|
}
|
|
a.display = display
|
|
display.SetErrorHandler(func(e client.DisplayErrorEvent) {
|
|
log.Printf("display error: object=%d code=%d %q", e.ObjectId, e.Code, e.Message)
|
|
})
|
|
|
|
registry, err := display.GetRegistry()
|
|
if err != nil {
|
|
return fmt.Errorf("get registry: %w", err)
|
|
}
|
|
a.registry = registry
|
|
registry.SetGlobalHandler(a.handleGlobal)
|
|
|
|
a.roundtrip() // register globals
|
|
a.roundtrip() // deliver their initial events
|
|
|
|
var missing []string
|
|
if a.shm == nil {
|
|
missing = append(missing, "wl_shm")
|
|
}
|
|
if a.copyMgr == nil {
|
|
missing = append(missing, "ext_image_copy_capture_manager_v1")
|
|
}
|
|
if a.toplevelSrcMgr == nil {
|
|
missing = append(missing, "ext_foreign_toplevel_image_capture_source_manager_v1")
|
|
}
|
|
if len(missing) > 0 {
|
|
return fmt.Errorf("compositor is missing required protocols: %s", strings.Join(missing, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *app) ctx() *client.Context { return a.display.Context() }
|
|
|
|
// bind is a spec-correct wl_registry.bind. go-wayland's Registry.Bind writes the
|
|
// string-length prefix as the *padded* length instead of len+1; older libwayland
|
|
// tolerated it, but newer libwayland rejects it with "invalid arguments". The
|
|
// proxy is already registered by its New* constructor, so we only marshal and
|
|
// send the request here.
|
|
func (a *app) bind(e client.RegistryGlobalEvent, p client.Proxy) {
|
|
iface := e.Interface
|
|
strLen := len(iface) + 1 // includes the null terminator
|
|
pad := strLen
|
|
if pad%4 != 0 {
|
|
pad += 4 - pad%4
|
|
}
|
|
const opcode = 0
|
|
bufLen := 8 + 4 + (4 + pad) + 4 + 4
|
|
buf := make([]byte, bufLen)
|
|
client.PutUint32(buf[0:4], a.registry.ID())
|
|
client.PutUint32(buf[4:8], uint32(bufLen<<16|opcode))
|
|
client.PutUint32(buf[8:12], e.Name)
|
|
client.PutString(buf[12:12+4+pad], iface, strLen)
|
|
o := 12 + 4 + pad
|
|
client.PutUint32(buf[o:o+4], e.Version)
|
|
client.PutUint32(buf[o+4:o+8], p.ID())
|
|
if err := a.ctx().WriteMsg(buf, nil); err != nil {
|
|
log.Fatalf("bind %s: %v", iface, err)
|
|
}
|
|
}
|
|
|
|
func (a *app) handleGlobal(e client.RegistryGlobalEvent) {
|
|
switch e.Interface {
|
|
case "wl_shm":
|
|
a.shm = client.NewShm(a.ctx())
|
|
a.bind(e, a.shm)
|
|
case "ext_image_copy_capture_manager_v1":
|
|
a.copyMgr = proto.NewExtImageCopyCaptureManager(a.ctx())
|
|
a.bind(e, a.copyMgr)
|
|
case "ext_foreign_toplevel_image_capture_source_manager_v1":
|
|
a.toplevelSrcMgr = proto.NewExtForeignToplevelImageCaptureSourceManager(a.ctx())
|
|
a.bind(e, a.toplevelSrcMgr)
|
|
case "ext_output_image_capture_source_manager_v1":
|
|
a.outputSrcMgr = proto.NewExtOutputImageCaptureSourceManager(a.ctx())
|
|
a.bind(e, a.outputSrcMgr)
|
|
case "ext_foreign_toplevel_list_v1":
|
|
a.toplevelList = proto.NewExtForeignToplevelList(a.ctx())
|
|
a.bind(e, a.toplevelList)
|
|
a.toplevelList.SetToplevelHandler(a.handleToplevel)
|
|
case "wl_output":
|
|
o := &output{wl: client.NewOutput(a.ctx())}
|
|
a.bind(e, o.wl)
|
|
a.outputs = append(a.outputs, o)
|
|
o.wl.SetGeometryHandler(func(e client.OutputGeometryEvent) { o.make_, o.model = e.Make, e.Model })
|
|
o.wl.SetNameHandler(func(e client.OutputNameEvent) { o.name = e.Name })
|
|
o.wl.SetDoneHandler(func(client.OutputDoneEvent) { o.done = true })
|
|
}
|
|
}
|
|
|
|
func (a *app) handleToplevel(e proto.ExtForeignToplevelListToplevelEvent) {
|
|
t := &toplevel{handle: e.Toplevel}
|
|
a.toplevels = append(a.toplevels, t)
|
|
t.handle.SetIdentifierHandler(func(e proto.ExtForeignToplevelHandleIdentifierEvent) { t.id = e.Identifier })
|
|
t.handle.SetTitleHandler(func(e proto.ExtForeignToplevelHandleTitleEvent) { t.title = e.Title })
|
|
t.handle.SetAppIdHandler(func(e proto.ExtForeignToplevelHandleAppIdEvent) { t.appID = e.AppId })
|
|
t.handle.SetClosedHandler(func(proto.ExtForeignToplevelHandleClosedEvent) { t.closed = true })
|
|
t.handle.SetDoneHandler(func(proto.ExtForeignToplevelHandleDoneEvent) { t.done = true })
|
|
}
|
|
|
|
// enumerate collects toplevels and outputs, round-tripping until each has
|
|
// delivered its full initial state (a "done" event) and no new ones appear.
|
|
func (a *app) enumerate() {
|
|
const maxRounds = 20 // safety bound; converges in 1-2 in practice
|
|
for range maxRounds {
|
|
before := len(a.toplevels)
|
|
a.roundtrip()
|
|
stable := len(a.toplevels) == before
|
|
allDone := true
|
|
for _, t := range a.toplevels {
|
|
if !t.done && !t.closed {
|
|
allDone = false
|
|
}
|
|
}
|
|
for _, o := range a.outputs {
|
|
if !o.done {
|
|
allDone = false
|
|
}
|
|
}
|
|
if stable && allDone {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// roundtrip flushes the connection and dispatches events until the compositor
|
|
// answers a sync callback, i.e. until everything queued so far is processed.
|
|
func (a *app) roundtrip() {
|
|
cb, err := a.display.Sync()
|
|
if err != nil {
|
|
log.Fatalf("sync: %v", err)
|
|
}
|
|
defer cb.Destroy()
|
|
done := false
|
|
cb.SetDoneHandler(func(client.CallbackDoneEvent) { done = true })
|
|
for !done {
|
|
if err := a.dispatch(); err != nil {
|
|
log.Fatalf("dispatch: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// dispatch processes one event, tolerating events addressed to objects we have
|
|
// already destroyed. The compositor may have queued events for a session/frame
|
|
// before it saw our destroy request; go-wayland reports those as "unable find
|
|
// sender" but has still consumed the message, so it is safe to continue. A real
|
|
// connection failure (EOF, broken pipe) is returned so callers can abort.
|
|
func (a *app) dispatch() error {
|
|
err := a.ctx().Dispatch()
|
|
if err != nil && strings.Contains(err.Error(), "unable find sender") {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// captureOne grabs a single frame for one source into an shm buffer and stores
|
|
// the raw pixels (and dimensions/transform/format) on the source. It returns
|
|
// false if the compositor could not satisfy the capture. All Wayland and mmap
|
|
// resources for this source are released before returning.
|
|
func (a *app) captureOne(s *source) bool {
|
|
session, err := a.copyMgr.CreateSession(s.cap, 0)
|
|
if err != nil {
|
|
log.Printf("create session %q: %v", s.id, err)
|
|
return false
|
|
}
|
|
defer session.Destroy()
|
|
defer s.cap.Destroy()
|
|
|
|
var w, h uint32
|
|
var formats []uint32
|
|
sessionDone := false
|
|
session.SetBufferSizeHandler(func(e proto.ExtImageCopyCaptureSessionBufferSizeEvent) { w, h = e.Width, e.Height })
|
|
session.SetShmFormatHandler(func(e proto.ExtImageCopyCaptureSessionShmFormatEvent) { formats = append(formats, e.Format) })
|
|
session.SetDoneHandler(func(proto.ExtImageCopyCaptureSessionDoneEvent) { sessionDone = true })
|
|
session.SetStoppedHandler(func(proto.ExtImageCopyCaptureSessionStoppedEvent) { sessionDone = true })
|
|
|
|
a.roundtrip()
|
|
if !sessionDone || w == 0 || h == 0 {
|
|
log.Printf("no buffer constraints for %q", s.id)
|
|
return false
|
|
}
|
|
|
|
format, swapRB, ok := chooseFormat(formats)
|
|
if !ok {
|
|
log.Printf("no supported shm format for %q (got %v)", s.id, formats)
|
|
return false
|
|
}
|
|
|
|
stride := int(w) * 4
|
|
size := stride * int(h)
|
|
fd, err := shmFile(size)
|
|
if err != nil {
|
|
log.Printf("shm file for %q: %v", s.id, err)
|
|
return false
|
|
}
|
|
defer unix.Close(fd)
|
|
|
|
data, err := unix.Mmap(fd, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
|
|
if err != nil {
|
|
log.Printf("mmap for %q: %v", s.id, err)
|
|
return false
|
|
}
|
|
defer unix.Munmap(data)
|
|
|
|
pool, err := a.shm.CreatePool(fd, int32(size))
|
|
if err != nil {
|
|
log.Printf("create pool for %q: %v", s.id, err)
|
|
return false
|
|
}
|
|
defer pool.Destroy()
|
|
|
|
buf, err := pool.CreateBuffer(0, int32(w), int32(h), int32(stride), uint32(format))
|
|
if err != nil {
|
|
log.Printf("create buffer for %q: %v", s.id, err)
|
|
return false
|
|
}
|
|
defer buf.Destroy()
|
|
|
|
frame, err := session.CreateFrame()
|
|
if err != nil {
|
|
log.Printf("create frame for %q: %v", s.id, err)
|
|
return false
|
|
}
|
|
defer frame.Destroy()
|
|
|
|
var tf uint32
|
|
ready, failed := false, false
|
|
frame.SetTransformHandler(func(e proto.ExtImageCopyCaptureFrameTransformEvent) { tf = e.Transform })
|
|
frame.SetReadyHandler(func(proto.ExtImageCopyCaptureFrameReadyEvent) { ready = true })
|
|
frame.SetFailedHandler(func(e proto.ExtImageCopyCaptureFrameFailedEvent) {
|
|
failed = true
|
|
log.Printf("capture failed for %q (reason=%d)", s.id, e.Reason)
|
|
})
|
|
|
|
if err := frame.AttachBuffer(buf); err != nil {
|
|
log.Printf("attach buffer for %q: %v", s.id, err)
|
|
return false
|
|
}
|
|
frame.DamageBuffer(0, 0, int32(w), int32(h))
|
|
if err := frame.Capture(); err != nil {
|
|
log.Printf("capture for %q: %v", s.id, err)
|
|
return false
|
|
}
|
|
|
|
for !ready && !failed {
|
|
if err := a.dispatch(); err != nil {
|
|
log.Printf("dispatch during capture %q: %v", s.id, err)
|
|
return false
|
|
}
|
|
}
|
|
if failed {
|
|
return false
|
|
}
|
|
|
|
// Copy the pixels out of the mapping before the deferred Munmap runs.
|
|
s.raw = make([]byte, size)
|
|
copy(s.raw, data)
|
|
s.w, s.h, s.transform, s.swapRB = w, h, tf, swapRB
|
|
return true
|
|
}
|
|
|
|
// shmFile returns an fd backed by an anonymous in-memory file of the given size.
|
|
func shmFile(size int) (int, error) {
|
|
fd, err := unix.MemfdCreate("wlthumbs", unix.MFD_CLOEXEC)
|
|
if err != nil {
|
|
return -1, fmt.Errorf("memfd_create: %w", err)
|
|
}
|
|
if err := unix.Ftruncate(fd, int64(size)); err != nil {
|
|
unix.Close(fd)
|
|
return -1, fmt.Errorf("ftruncate: %w", err)
|
|
}
|
|
return fd, nil
|
|
}
|