golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
124.3k stars 17.7k forks source link

x/exp/shiny/driver/gldriver: deadlock when creating multiple windows on darwin #14294

Open josharian opened 8 years ago

josharian commented 8 years ago

I am attempting to create multiple windows, to avoid having to write any layout or windowing code myself.

func main() {
    driver.Main(func(s screen.Screen) {
        w, err := s.NewWindow(nil)
        if err != nil {
            log.Fatal(err)
        }
        w2, err := s.NewWindow(nil)
        if err != nil {
            log.Fatal(err)
        }
        _, _ = w, w2
    })
}

This hangs at the second call to NewWindow. Backtrace:

SIGQUIT: quit
PC=0x405bacb m=0
signal arrived during cgo execution

goroutine 1 [chan receive, locked to thread]:
runtime.mach_semaphore_wait(0xc800001003, 0x4228e00, 0x4031592, 0x40e1d00, 0x7fff5fbfd480, 0x4228e00, 0x404fb79, 0xffffffffffffffff, 0x419aba0, 0x7fff5fbfd494, ...)
    $GOROOT/src/runtime/sys_darwin_amd64.s:411 +0xb fp=0x7fff5fbfd428 sp=0x7fff5fbfd420
runtime.semasleep1(0xffffffffffffffff, 0x419aba0)
    $GOROOT/src/runtime/os1_darwin.go:423 +0xdf fp=0x7fff5fbfd460 sp=0x7fff5fbfd428
runtime.semasleep.func1()
    $GOROOT/src/runtime/os1_darwin.go:439 +0x29 fp=0x7fff5fbfd480 sp=0x7fff5fbfd460
runtime.systemstack(0x7fff5fbfd498)
    $GOROOT/src/runtime/asm_amd64.s:307 +0xab fp=0x7fff5fbfd488 sp=0x7fff5fbfd480
runtime.semasleep(0xffffffffffffffff, 0x0)
    $GOROOT/src/runtime/os1_darwin.go:440 +0x36 fp=0x7fff5fbfd4b8 sp=0x7fff5fbfd488
runtime.notesleep(0x4229248)
    $GOROOT/src/runtime/lock_sema.go:166 +0xed fp=0x7fff5fbfd4f0 sp=0x7fff5fbfd4b8
runtime.stoplockedm()
    $GOROOT/src/runtime/proc.go:1682 +0xae fp=0x7fff5fbfd510 sp=0x7fff5fbfd4f0
runtime.schedule()
    $GOROOT/src/runtime/proc.go:2030 +0x6e fp=0x7fff5fbfd548 sp=0x7fff5fbfd510
runtime.park_m(0xc820000180)
    $GOROOT/src/runtime/proc.go:2137 +0x18b fp=0x7fff5fbfd570 sp=0x7fff5fbfd548
runtime.mcall(0x4059e1a)
    $GOROOT/src/runtime/asm_amd64.s:233 +0x5b fp=0x7fff5fbfd580 sp=0x7fff5fbfd570

goroutine 17 [syscall, locked to thread]:
runtime.goexit()
    $GOROOT/src/runtime/asm_amd64.s:1998 +0x1

goroutine 19 [syscall, locked to thread]:
golang.org/x/exp/shiny/driver/gldriver._Cfunc_doNewWindow(0x30000000400, 0x0)
    golang.org/x/exp/shiny/driver/gldriver/_obj/_cgo_gotypes.go:200 +0x42
golang.org/x/exp/shiny/driver/gldriver.newWindow(0x0, 0x413f320, 0x0, 0x0)
    golang.org/x/exp/shiny/driver/gldriver/cocoa.go:62 +0x63
golang.org/x/exp/shiny/driver/gldriver.(*screenImpl).NewWindow(0x4228a40, 0x0, 0x0, 0x0, 0x0, 0x0)
    golang.org/x/exp/shiny/driver/gldriver/screen.go:125 +0x4c
main.main.func1(0x5aab2b8, 0x4228a40)
    main.go:35 +0x14f
golang.org/x/exp/shiny/driver/gldriver.driverStarted.func1()
    golang.org/x/exp/shiny/driver/gldriver/cocoa.go:102 +0x47
created by golang.org/x/exp/shiny/driver/gldriver.driverStarted
    golang.org/x/exp/shiny/driver/gldriver/cocoa.go:104 +0x2b

goroutine 20 [select, locked to thread]:
golang.org/x/exp/shiny/driver/gldriver.drawLoop(0xc820092000, 0x1)
    golang.org/x/exp/shiny/driver/gldriver/cocoa.go:147 +0x482
created by golang.org/x/exp/shiny/driver/gldriver.preparedOpenGL
    golang.org/x/exp/shiny/driver/gldriver/cocoa.go:80 +0x117

The interesting goroutine is goroutine 19, which is blocked in the cgo doNewWindow call, at dispatch_sync(dispatch_get_main_queue(), .... I've confirmed that at the time of the call, the current thread and the main thread are different. So either (a) the main thread is blocked somewhere else, (b) the main thread's run loop is stopped, or (c) the main thread's run loop is in the wrong mode.

Workaround suggestions would be very welcome, no matter how hacky.

/cc @nigeltao @crawshaw

nigeltao commented 8 years ago

Yeah, your Go code is fine, and shiny is designed to handle multiple windows, but I'm not sure if the gldriver actually implements that yet or if it's just a TODO. There's certainly a TODO about it for the X11 flavor of gldriver (not to be mistaken for x11driver), and I wouldn't be surprised if some work remains on the Cocoa flavor. It's been a long time since I did any Obj-C / Cocoa programming, though, so I'm not exactly sure what needs doing, and what subtleties there are around trying to use multiple windows (and multiple OpenGL contexts). @crawshaw knows this better than I do, but I can't speak for when he will have any time to look at this.

crawshaw commented 8 years ago

On gldriver/cocoa at least, the problem is much simpler:

func main() {
    driver.Main(func(s screen.Screen) {
        w, err := s.NewWindow(nil)
        if err != nil {
            log.Fatal(err)
        }
        _ = w
        select{}
    })
}

In this code, the window never appears and the main draw loop isn't running. Cocoa is waiting for something, and I'm not sure what. In particular, doShowWindow is called and returns, but the async event it sends to the main cocoa thread is never run.

I'll try to poke at this a bit more between meetings today.

crawshaw commented 8 years ago

OK, it's getting stuck in - (void)drawRect.

Approximately: Cocoa wants the first frame to be drawn before displaying the window, which means the main thread is blocked until w.Publish() is called on the new window.

So a quick workaround is to start the event loop for the first window before opening the second:

package main                                                                         

import (                                                                             
        "log"                                                                        

        "golang.org/x/exp/shiny/driver"                                              
        "golang.org/x/exp/shiny/screen"                                              
        "golang.org/x/mobile/event/paint"                                            
)                                                                                    

func main() {                                                                        
        driver.Main(func(s screen.Screen) {                                          
                openWindow(s, "window 1")                                            
                openWindow(s, "window 2")                                            
                select {}                                                            
        })                                                                           
}                                                                                    

func openWindow(s screen.Screen, name string) {                                      
        w, err := s.NewWindow(nil)                                                   
        if err != nil {                                                              
                log.Fatal(err)                                                       
        }                                                                            
        go func() {                                                                  
                for {                                                                
                        e := w.NextEvent()                                           
                        log.Printf("%s: %#+v", name, e)                              
                        switch e.(type) {                                            
                        case paint.Event:                                            
                                w.Publish()                                          
                        }                                                            
                }                                                                    
        }()                                                                          
}                                                                                    
josharian commented 8 years ago

Thanks, @crawshaw! That works. Unfortunately, I'm getting glitchy output (intermittent blinking black window content) when doing that. Maybe Publish or some of the other drawing functionality isn't quite concurrency-safe yet.

For the moment, I'll just write some simple/throwaway layout code and use a single window.