Skip to content
Open
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
15 changes: 15 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: ci
on:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.26"
cache: true
- run: go vet ./...
- run: go build ./...
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ go 1.26.3

require (
github.com/LocalKinAI/sckit-go v0.3.1 // indirect
github.com/ebitengine/purego v0.8.0 // indirect
github.com/ebitengine/purego v0.8.0
)
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@ import (
"sync"

"distancedesktop/captured/pipelines"
"distancedesktop/captured/pipelines/linux"
"distancedesktop/captured/pipelines/macos"
)

var pipeline pipelines.Pipeline

func main() {
listen := flag.String("listen", "", "TCP address for remote control (e.g. :9090)")
source := flag.String("source", "kms", "capture source on linux: kms|x11")
flag.Parse()

switch runtime.GOOS {
case "darwin":
pipeline = macos.New()
case "linux":
pipeline = linux.New(*source)
default:
log.Fatalf("unsupported platform: %s", runtime.GOOS)
}
Expand Down
141 changes: 141 additions & 0 deletions pipelines/linux/gbm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//go:build linux

package linux

import (
"fmt"
"os"
"sync"

"github.com/ebitengine/purego"
)

// ---------------------------------------------------------------------------
// libgbm bindings. Used by the synthetic source to allocate a real linear
// GBM BO, export it as a dma-buf and mmap it — the same primitive the future
// DMA-BUF -> nvh264enc encode path will reuse.
// ---------------------------------------------------------------------------

const gbmLibName = "libgbm.so.1"

const (
gbmFormatXRGB8888 = 0x34325258
gbmBoUseRendering = 1 << 2
gbmBoUseWrite = 1 << 3
gbmBoUseLinear = 1 << 4
)

var (
gbmOnce sync.Once
gbmLib uintptr

gbmCreateDevice func(fd int) uintptr
gbmDeviceDestroy func(dev uintptr)
gbmBoCreate func(dev uintptr, w, h, format, usage uint32) uintptr
gbmBoDestroy func(bo uintptr)
gbmBoGetFD func(bo uintptr) int
gbmBoGetStride func(bo uintptr) uint32
)

func loadGBM() {
gbmOnce.Do(func() {
h, err := purego.Dlopen(gbmLibName, purego.RTLD_NOW|purego.RTLD_GLOBAL)
if err != nil {
gbmLib = 0
return
}
gbmLib = h
purego.RegisterLibFunc(&gbmCreateDevice, gbmLib, "gbm_create_device")
purego.RegisterLibFunc(&gbmDeviceDestroy, gbmLib, "gbm_device_destroy")
purego.RegisterLibFunc(&gbmBoCreate, gbmLib, "gbm_bo_create")
purego.RegisterLibFunc(&gbmBoDestroy, gbmLib, "gbm_bo_destroy")
purego.RegisterLibFunc(&gbmBoGetFD, gbmLib, "gbm_bo_get_fd")
purego.RegisterLibFunc(&gbmBoGetStride, gbmLib, "gbm_bo_get_stride")
})
}

// gbmBO wraps a linear XRGB8888 GBM buffer mapped for writing.
type gbmBO struct {
boH uintptr
devH uintptr
fd int // dma-buf fd
stride uint32
w, h int
mapped []byte
dev *os.File // keeps the DRM fd alive for the lifetime of the device
}

// newGBMBuffer creates a linear XRGB8888 GBM buffer on the given DRM device
// path, exports it as a dma-buf and maps it read/write.
func newGBMBuffer(devPath string, w, h int) (*gbmBO, error) {
loadGBM()
if gbmLib == 0 {
return nil, fmt.Errorf("libgbm (%s) unavailable", gbmLibName)
}
f, err := os.OpenFile(devPath, os.O_RDWR, 0)
if err != nil {
return nil, err
}
devH := gbmCreateDevice(int(f.Fd()))
if devH == 0 {
f.Close()
return nil, fmt.Errorf("gbm_create_device failed")
}
bo := gbmBoCreate(devH, uint32(w), uint32(h), gbmFormatXRGB8888,
gbmBoUseRendering|gbmBoUseLinear)
if bo == 0 {
gbmDeviceDestroy(devH)
f.Close()
return nil, fmt.Errorf("gbm_bo_create failed")
}
stride := gbmBoGetStride(bo)
dmabuf := gbmBoGetFD(bo)
if dmabuf < 0 {
gbmBoDestroy(bo)
gbmDeviceDestroy(devH)
f.Close()
return nil, fmt.Errorf("gbm_bo_get_fd failed")
}
mapped, err := mmapRW(dmabuf, int(stride)*h)
if err != nil {
closeFD(dmabuf)
gbmBoDestroy(bo)
gbmDeviceDestroy(devH)
f.Close()
return nil, fmt.Errorf("mmap gbm bo: %w", err)
}
return &gbmBO{
boH: bo,
devH: devH,
fd: dmabuf,
stride: stride,
w: w,
h: h,
mapped: mapped,
dev: f,
}, nil
}

// Pixels returns the mapped buffer (XRGB8888 layout: B,G,R,X per pixel).
func (b *gbmBO) Pixels() []byte { return b.mapped }

// Stride returns the row stride in bytes.
func (b *gbmBO) Stride() int { return int(b.stride) }

func (b *gbmBO) Close() {
if b.mapped != nil {
munmap(b.mapped)
}
if b.fd >= 0 {
closeFD(b.fd)
}
if b.boH != 0 {
gbmBoDestroy(b.boH)
}
if b.devH != 0 {
gbmDeviceDestroy(b.devH)
}
if b.dev != nil {
b.dev.Close()
}
}
Loading
Loading