diff --git a/internal/usbgadget/config.go b/internal/usbgadget/config.go index dad5b79..3b98aca 100644 --- a/internal/usbgadget/config.go +++ b/internal/usbgadget/config.go @@ -201,6 +201,9 @@ func (u *UsbGadget) Init() error { return u.logError("unable to initialize USB stack", err) } + // Pre-open HID files to reduce input latency + u.PreOpenHidFiles() + return nil } diff --git a/internal/usbgadget/hid_keyboard.go b/internal/usbgadget/hid_keyboard.go index 6ad3b6a..14b054b 100644 --- a/internal/usbgadget/hid_keyboard.go +++ b/internal/usbgadget/hid_keyboard.go @@ -203,8 +203,7 @@ func (u *UsbGadget) keyboardWriteHidFile(data []byte) error { _, err := u.keyboardHidFile.Write(data) if err != nil { u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0") - u.keyboardHidFile.Close() - u.keyboardHidFile = nil + // Keep file open on write errors to reduce I/O overhead return err } u.resetLogSuppressionCounter("keyboardWriteHidFile") diff --git a/internal/usbgadget/hid_mouse_absolute.go b/internal/usbgadget/hid_mouse_absolute.go index 2718f20..ec1d730 100644 --- a/internal/usbgadget/hid_mouse_absolute.go +++ b/internal/usbgadget/hid_mouse_absolute.go @@ -77,8 +77,7 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error { _, err := u.absMouseHidFile.Write(data) if err != nil { u.logWithSuppression("absMouseWriteHidFile", 100, u.log, err, "failed to write to hidg1") - u.absMouseHidFile.Close() - u.absMouseHidFile = nil + // Keep file open on write errors to reduce I/O overhead return err } u.resetLogSuppressionCounter("absMouseWriteHidFile") diff --git a/internal/usbgadget/hid_mouse_relative.go b/internal/usbgadget/hid_mouse_relative.go index 786f265..6ece51f 100644 --- a/internal/usbgadget/hid_mouse_relative.go +++ b/internal/usbgadget/hid_mouse_relative.go @@ -60,15 +60,14 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error { var err error u.relMouseHidFile, err = os.OpenFile("/dev/hidg2", os.O_RDWR, 0666) if err != nil { - return fmt.Errorf("failed to open hidg1: %w", err) + return fmt.Errorf("failed to open hidg2: %w", err) } } _, err := u.relMouseHidFile.Write(data) if err != nil { u.logWithSuppression("relMouseWriteHidFile", 100, u.log, err, "failed to write to hidg2") - u.relMouseHidFile.Close() - u.relMouseHidFile = nil + // Keep file open on write errors to reduce I/O overhead return err } u.resetLogSuppressionCounter("relMouseWriteHidFile") diff --git a/internal/usbgadget/usbgadget.go b/internal/usbgadget/usbgadget.go index f51050b..af078dc 100644 --- a/internal/usbgadget/usbgadget.go +++ b/internal/usbgadget/usbgadget.go @@ -95,6 +95,33 @@ func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger * return newUsbGadget(name, defaultGadgetConfig, enabledDevices, config, logger) } +// PreOpenHidFiles opens all HID files to reduce input latency +func (u *UsbGadget) PreOpenHidFiles() { + if u.enabledDevices.Keyboard { + if err := u.openKeyboardHidFile(); err != nil { + u.log.Debug().Err(err).Msg("failed to pre-open keyboard HID file") + } + } + if u.enabledDevices.AbsoluteMouse { + if u.absMouseHidFile == nil { + var err error + u.absMouseHidFile, err = os.OpenFile("/dev/hidg1", os.O_RDWR, 0666) + if err != nil { + u.log.Debug().Err(err).Msg("failed to pre-open absolute mouse HID file") + } + } + } + if u.enabledDevices.RelativeMouse { + if u.relMouseHidFile == nil { + var err error + u.relMouseHidFile, err = os.OpenFile("/dev/hidg2", os.O_RDWR, 0666) + if err != nil { + u.log.Debug().Err(err).Msg("failed to pre-open relative mouse HID file") + } + } + } +} + func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget { if logger == nil { logger = defaultLogger diff --git a/jsonrpc.go b/jsonrpc.go index d79e10e..94bd486 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -10,7 +10,6 @@ import ( "path/filepath" "reflect" "strconv" - "sync" "time" "github.com/pion/webrtc/v4" @@ -19,73 +18,7 @@ import ( "github.com/jetkvm/kvm/internal/usbgadget" ) -// Mouse event processing with single worker -var ( - mouseEventChan = make(chan mouseEventData, 100) // Buffered channel for mouse events - mouseWorkerOnce sync.Once -) - -type mouseEventData struct { - message webrtc.DataChannelMessage - session *Session -} - -// startMouseWorker starts a single worker goroutine for processing mouse events -func startMouseWorker() { - go func() { - ticker := time.NewTicker(16 * time.Millisecond) // ~60 FPS - defer ticker.Stop() - - var latestMouseEvent *mouseEventData - - for { - select { - case event := <-mouseEventChan: - // Always keep the latest mouse event - latestMouseEvent = &event - - case <-ticker.C: - // Process the latest mouse event at regular intervals - if latestMouseEvent != nil { - onRPCMessage(latestMouseEvent.message, latestMouseEvent.session) - latestMouseEvent = nil - } - } - } - }() -} - -// onRPCMessageThrottled handles RPC messages with special throttling for mouse events -func onRPCMessageThrottled(message webrtc.DataChannelMessage, session *Session) { - var request JSONRPCRequest - err := json.Unmarshal(message.Data, &request) - if err != nil { - onRPCMessage(message, session) - return - } - - // Check if this is a mouse event that should be throttled - if isMouseEvent(request.Method) { - // Start the mouse worker if not already started - mouseWorkerOnce.Do(startMouseWorker) - - // Send to mouse worker (non-blocking) - select { - case mouseEventChan <- mouseEventData{message: message, session: session}: - // Event queued successfully - default: - // Channel is full, drop the event (this prevents blocking) - } - } else { - // Non-mouse events are processed immediately - go onRPCMessage(message, session) - } -} - -// isMouseEvent checks if the RPC method is a mouse-related event -func isMouseEvent(method string) bool { - return method == "absMouseReport" || method == "relMouseReport" -} +// Direct RPC message handling for optimal input responsiveness type JSONRPCRequest struct { JSONRPC string `json:"jsonrpc"` diff --git a/webrtc.go b/webrtc.go index edbcd00..a67460a 100644 --- a/webrtc.go +++ b/webrtc.go @@ -119,7 +119,7 @@ func newSession(config SessionConfig) (*Session, error) { case "rpc": session.RPCChannel = d d.OnMessage(func(msg webrtc.DataChannelMessage) { - go onRPCMessageThrottled(msg, session) + go onRPCMessage(msg, session) }) triggerOTAStateUpdate() triggerVideoStateUpdate()