我正在为一个相当复杂的视频游戏做一些概念验证工作,我想使用 HOpenGL 库在 Haskell 中编写。我首先编写了一个实现基于客户端-服务器事件的通信的模块。当我尝试将它连接到一个简单的程序以在屏幕上绘制点击时,我的问题出现了。
事件库使用一个 TChans 列表组成一个优先级队列进行通信。它返回对应于服务器绑定和客户端绑定消息的“出”队列和“入”队列。使用 forkIO 在不同的线程中发送和接收事件。在没有 OpenGL 部分的情况下测试事件库表明它通信成功。这是我用来测试它的代码:
-- Client connects to server at localhost with 3 priorities in the priority queue
do { (outQueue, inQueue) <- client Nothing 3
-- send 'Click' events until terminated, the server responds with the coords negated
; mapM_ (\x -> atomically $ writeThing outQueue (lookupPriority x) x)
(repeat (Click (fromIntegral 2) (fromIntegral 4)))
}
这会产生预期的输出,即大量的发送和接收事件。我认为问题不在于事件处理库。
代码的 OpenGL 部分在 displayCallback 中检查传入队列中的新事件,然后调用事件的关联处理程序。我可以让 displayCallback 捕获一个事件(Init 事件,它只是清除屏幕),但之后什么都没有捕获。以下是相关代码:
atomically $ PQ.writeThing inqueue (Events.lookupPriority Events.Init) Events.Init
GLUT.mainLoop
render pqueue =
do event <- atomically $
do e <- PQ.getThing pqueue
case e of
Nothing -> retry
Just event -> return event
putStrLn $ "Got event"
(Events.lookupHandler event Events.Client) event
GL.flush
GLUT.swapBuffers
所以我关于为什么会发生这种情况的理论是:
- 显示回调在重试时阻塞了所有发送和接收线程。
- 队列未正确返回,因此客户端读取的队列与 OpenGL 部分读取的队列不同。
还有其他原因导致这种情况发生吗?
完整的代码太长了,虽然不是太长(每个 100 行以下 5 个文件),但这里发布的代码太长了,但它都在 GitHub上。
编辑 1:
客户端从 HOpenGL 代码的主函数中运行,如下所示:
main =
do args <- getArgs
let ip = args !! 0
let priorities = args !! 1
(progname, _) <- GLUT.getArgsAndInitialize
-- Run the client here and bind the queues to use for communication
(outqueue, inqueue) <- Client.client (Just ip) priorities
GLUT.createWindow "Hello World"
GLUT.initialDisplayMode $= [GLUT.DoubleBuffered, GLUT.RGBAMode]
GLUT.keyboardMouseCallback $= Just (keyboardMouse outqueue)
GLUT.displayCallback $= render inqueue
PQ.writeThing inqueue (Events.lookupPriority Events.Init) Events.Init
GLUT.mainLoop
我在编译代码时传递给 GHC 的唯一标志是-package GLUT
.
编辑 2:
我清理了 Github 上的代码。我删除了acceptInput,因为它实际上并没有做任何事情,而且客户端代码不应该监听它自己的事件,这就是它返回队列的原因。
编辑3:
我稍微澄清一下我的问题。我从@Shang 和@Laar 那里学到了一些东西,然后就跟着跑了。我将 Client.hs 中的线程更改为使用 forkOS 而不是 forkIO (并在 ghc 中使用 -threading),看起来事件正在成功通信,但是在显示回调中没有收到它们。我也尝试postRedisplay
在显示回调结束时调用,但我认为它不会被调用(因为我认为重试阻塞了整个 OpenGL 线程)。
显示回调中的重试会阻塞整个 OpenGL 线程吗?如果是这样,将显示回调分叉到新线程中是否安全?我不认为它会,因为可能存在多个事物可能同时尝试绘制到屏幕上的可能性,但我可能能够用锁来处理它。另一种解决方案是将lookupHandler
函数转换为返回包装在 a 中的函数Maybe
,如果没有任何事件,则什么也不做。我觉得这不太理想,因为我基本上会有一个繁忙的循环,这是我试图避免的事情。
编辑 4:
忘了提到我在做 forkOS 时在 ghc 使用了 -threading。
编辑5:
我去测试了我的理论,即渲染函数(显示回调)中的重试阻塞了所有的OpenGL。我重写了渲染函数,使它不再阻塞,它就像我想要的那样工作。屏幕上的一次点击给出两点,一个来自服务器,一个来自原始点击。这是新渲染函数的代码(注意:它不在Github 中):
render pqueue =
do event <- atomically $ PQ.getThing pqueue
case (Events.lookupHandler event Events.Client) of
Nothing -> return ()
Just handler ->
do let e = case event of {Just e' -> e'}
handler e
return ()
GL.flush
GLUT.swapBuffers
GLUT.postRedisplay Nothing
我在有和没有 postRedisplay 的情况下都试过了,它只适用于它。现在的问题是,这将 CPU 固定在 100%,因为它是一个繁忙的循环。在编辑 4 中,我建议线程化显示回调。我还在想办法做到这一点。
一个注释,因为我还没有提到它。任何想要构建/运行代码的人都应该这样做:
$ ghc -threaded -package GLUT helloworldOGL.hs -o helloworldOGL
$ ghc server.hs -o server
-- one or the other, I usually do 0.0.0.0
$ ./server "localhost" 3
$ ./server "0.0.0.0" 3
$ ./helloworldOGL "localhost" 3
编辑 6:解决方案
解决方案!与线程一起,我决定在 OpenGL 代码中创建一个线程来检查事件,如果没有则阻塞,然后调用处理程序,然后调用 postRedisplay。这里是:
checkEvents pqueue = forever $
do event <- atomically $
do e <- PQ.getThing pqueue
case e of
Nothing -> retry
Just event -> return event
putStrLn $ "Got event"
(Events.lookupHandler event Events.Client) event
GLUT.postRedisplay Nothing
显示回调很简单:
render = GLUT.swapBuffers
而且它有效,它不会将 CPU 固定为 100%,并且事件会得到及时处理。我在这里发布这个是因为没有其他答案我无法做到这一点,而且当答案都非常有帮助时,我很难接受代表,所以我接受@Laar 的回答,因为他的代表较低。