vncgl/vncgl.go (263 lines of code) (raw):

package vncgl import ( "image" _ "image/png" "net" "runtime" "sync" "time" "github.com/go-gl/gl/v2.1/gl" "github.com/go-gl/glfw/v3.2/glfw" "github.com/juju/errors" logging "github.com/op/go-logging" "github.com/openai/go-vncdriver/vncclient" ) var ( log = logging.MustGetLogger("vncgl") setup sync.Once ) func colorsToImage(x, y, width, height uint16, colors []vncclient.Color) *image.RGBA { imgRect := image.Rect(int(x), int(y), int(x+width), int(y+height)) rgba := image.NewRGBA(imgRect) for i, color := range colors { rgba.Pix[4*i] = color.R rgba.Pix[4*i+1] = color.G rgba.Pix[4*i+2] = color.B rgba.Pix[4*i+3] = 1.0 } return rgba } func SetupRendering() { setup.Do(func() { if err := glfw.Init(); err != nil { log.Fatalf("failed to initialize glfw: %v", err) } glfw.WindowHint(glfw.Resizable, glfw.False) glfw.WindowHint(glfw.ContextVersionMajor, 2) glfw.WindowHint(glfw.ContextVersionMinor, 1) }) } // Only interact with this from the main thread type VNCGL struct { rootTexture uint32 window *glfw.Window windowWidth, windowHeight uint16 closed bool } func (v *VNCGL) Init(width, height uint16, name string, screen []vncclient.Color) error { window, err := glfw.CreateWindow(int(width), int(height), name, nil, nil) if err != nil { return errors.Annotate(err, "couldn't create window") } window.MakeContextCurrent() glfw.SwapInterval(0) if err := gl.Init(); err != nil { window.Destroy() return errors.Annotate(err, "could not init opengl") } v.window = window v.windowWidth = width v.windowHeight = height if screen != nil { image := colorsToImage(0, 0, width, height, screen) v.applyImage(image) } return nil } func (g *VNCGL) Close() error { if g.closed { return nil } g.closed = true g.window.Destroy() if g.rootTexture != 0 { gl.DeleteTextures(1, &g.rootTexture) } return nil } func (g *VNCGL) Apply(updates []*vncclient.FramebufferUpdateMessage) { start := time.Now().UnixNano() count := 0 for _, update := range updates { for _, rect := range update.Rectangles { count += 1 var rgba *image.RGBA switch enc := rect.Enc.(type) { case *vncclient.RawEncoding: rgba = colorsToImage(rect.X, rect.Y, rect.Width, rect.Height, enc.Colors) case *vncclient.ZRLEEncoding: rgba = colorsToImage(rect.X, rect.Y, rect.Width, rect.Height, enc.Colors) case *vncclient.TightEncoding: rgba = colorsToImage(rect.X, rect.Y, rect.Width, rect.Height, enc.Colors) default: panic(errors.Errorf("BUG: unrecognized encoding: %+v", enc)) } g.applyImage(rgba) } } log.Debugf("Completed Apply: count=%d time=%v", count, time.Duration(time.Now().UnixNano()-start)) } func (g *VNCGL) applyImage(img *image.RGBA) { // TODO: make sure texture can't legitimately be 0 if g.rootTexture == 0 { g.rootTexture = newTexture(img) } else { gl.BindTexture(gl.TEXTURE_2D, g.rootTexture) gl.TexSubImage2D( gl.TEXTURE_2D, 0, int32(img.Rect.Min.X), int32(img.Rect.Min.Y), int32(img.Rect.Size().X), int32(img.Rect.Size().Y), gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(img.Pix)) } } func newTexture(rgba *image.RGBA) uint32 { var texture uint32 gl.Enable(gl.TEXTURE_2D) gl.GenTextures(1, &texture) gl.BindTexture(gl.TEXTURE_2D, texture) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) gl.TexImage2D( gl.TEXTURE_2D, 0, gl.RGBA, int32(rgba.Rect.Size().X), int32(rgba.Rect.Size().Y), 0, gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(rgba.Pix)) return texture } func (g *VNCGL) Render() { if g.closed { return } glfw.PollEvents() if g.window.ShouldClose() { g.Close() return } g.window.MakeContextCurrent() g.drawScene() gl.Finish() start := time.Now().UnixNano() g.window.SwapBuffers() // Usually takes like 100microseconds, but sometimes takes up // to 10ms. log.Debugf("SwapBuffers: time=%v", time.Duration(time.Now().UnixNano()-start)) } func (g *VNCGL) drawScene() { gl.MatrixMode(gl.PROJECTION) gl.LoadIdentity() gl.Ortho(0, float64(g.windowWidth), 0, float64(g.windowHeight), -1, 1) gl.Enable(gl.TEXTURE_2D) gl.Clear(gl.COLOR_BUFFER_BIT) gl.BindTexture(gl.TEXTURE_2D, g.rootTexture) gl.Begin(gl.QUADS) gl.TexCoord2d(0.0, 0.0) gl.Vertex3f(0, float32(g.windowHeight), 0.0) gl.TexCoord2d(1.0, 0.0) gl.Vertex3f(float32(g.windowWidth), float32(g.windowHeight), 0.0) gl.TexCoord2d(1.0, 1.0) gl.Vertex3f(float32(g.windowWidth), 0.0, 0.0) gl.TexCoord2d(0.0, 1.0) gl.Vertex3f(0, 0.0, 0.0) gl.End() gl.PopMatrix() // gl.Flush() } func init() { // GLFW event handling must run on the main OS thread runtime.LockOSThread() } func connect(imgs chan *image.RGBA) (*vncclient.ClientConn, error) { tcp, err := net.Dial("tcp", "172.16.163.128:5900") if err != nil { return nil, errors.Annotate(err, "could not connect to server") } errCh := make(chan error) serverMessageCh := make(chan vncclient.ServerMessage) conn, err, _ := vncclient.Client(tcp, &vncclient.ClientConfig{ Auth: []vncclient.ClientAuth{ &vncclient.PasswordAuth{ Password: "openai", }, }, ServerMessageCh: serverMessageCh, ErrorCh: errCh, }) if err != nil { return nil, errors.Annotate(err, "could not establish VNC connection to server") } go func() { err := <-errCh panic(errors.ErrorStack(err)) }() updates := make(chan *vncclient.FramebufferUpdateMessage) // Message thread go func() { // Ensure standard format conn.SetPixelFormat(&vncclient.PixelFormat{ BPP: 32, Depth: 24, BigEndian: false, TrueColor: true, RedMax: 255, GreenMax: 255, BlueMax: 255, RedShift: 0, GreenShift: 8, BlueShift: 16, }) conn.SetEncodings([]vncclient.Encoding{ &vncclient.ZRLEEncoding{}, &vncclient.TightEncoding{}, &vncclient.RawEncoding{}, }) conn.FramebufferUpdateRequest(true, 0, 0, conn.FramebufferWidth, conn.FramebufferHeight) for { select { case msg := <-serverMessageCh: switch msg := msg.(type) { case *vncclient.FramebufferUpdateMessage: updates <- msg // Keep re-requesting! conn.FramebufferUpdateRequest(true, 0, 0, conn.FramebufferWidth, conn.FramebufferHeight) } } } }() vncgl := &VNCGL{} if err := vncgl.Init(conn.FramebufferWidth, conn.FramebufferHeight, conn.DesktopName, nil); err != nil { return nil, err } // Apply those updates var frames int target := time.Now().UnixNano() printTarget := time.Now().UnixNano() for { target += int64(1000 * 1000 * 1000 / 60) frames += 1 if time.Now().UnixNano() > printTarget { printTarget = time.Now().UnixNano() + 1000*1000*1000 log.Infof("effective framerate: %v", frames) frames = 0 } var done bool for !done { select { case msg := <-updates: msgs := []*vncclient.FramebufferUpdateMessage{msg} vncgl.Apply(msgs) default: done = true } } vncgl.Render() delta := time.Duration(target-time.Now().UnixNano()) * time.Nanosecond if delta > 0 { log.Infof("sleeping: %v", delta) time.Sleep(delta) } else { log.Infof("fell behind by: %v", delta) } } }