windows pyglet 2.1.13 python3.13 再 pyglet窗口设置全屏并给定指定的长宽,且长宽都是屏幕支持的mode,当很小的分辨率窗口,因为指定pyglet window的更大的分辨率,会切换系统的分辨率。window对象创建都ok,但是当跑的时候,就会报错,这种情况在MacOS完全不存在。
pyglet.app.run()
event_loop.run(interval)
~~~~~~~~~~~~~~^^^^^^^^^^
timeout = self.idle()
self.clock.call_scheduled_functions(dt)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^
File "C:\Users\...\AppData\Local\Programs\Python\Python313\Lib\site-packages\pyglet\clock.py", line 217, in call_scheduled_functions
item.func(now - item.last_ts, *item.args, **item.kwargs)
~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\...\AppData\Local\Programs\Python\Python313\Lib\site-packages\pyglet\app\base.py", line 113, in _redraw_windows
window.draw(dt)
~~~~~~~~~~~^^^^
File "C:\Users\...\AppData\Local\Programs\Python\Python313\Lib\site-packages\pyglet\window\__init__.py", line 711, in draw
self.dispatch_event('on_draw')
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
File "C:\Users\...\AppData\Local\Programs\Python\Python313\Lib\site-packages\pyglet\window\__init__.py", line 685, in dispatch_event
super().dispatch_event(*args)
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
File "C:\Users\...\AppData\Local\Programs\Python\Python313\Lib\site-packages\pyglet\event.py", line 364, in dispatch_event
if handler(*args):
~~~~~~~^^^^^^^
File "C:\Users\...game_main.py", line 25, in on_draw
func()
~~~~^^
File "C:\Users\...scene_init.py", line 44, in on_draw
batch.draw()
~~~~~~~~~~^^
File "C:\Users\...\AppData\Local\Programs\Python\Python313\Lib\site-packages\pyglet\graphics\__init__.py", line 558, in draw
func()
~~~~^^
File "C:\Users\...\AppData\Local\Programs\Python\Python313\Lib\site-packages\pyglet\graphics\__init__.py", line 488, in <lambda>
draw_list.append((lambda d, m: lambda: d.draw(m))(domain, mode)) # noqa: PLC3002
~~~~~~^^^
File "C:\Users\...\AppData\Local\Programs\Python\Python313\Lib\site-packages\pyglet\graphics\vertexdomain.py", line 472, in draw
self.vao.bind()
~~~~~~~~~~~~~^^
File "C:\Users\...\AppData\Local\Programs\Python\Python313\Lib\site-packages\pyglet\graphics\vertexarray.py", line 32, in bind
glBindVertexArray(self._id)
~~~~~~~~~~~~~~~~~^^^^^^^^^^
File "C:\Users\...\AppData\Local\Programs\Python\Python313\Lib\site-packages\pyglet\gl\lib.py", line 83, in errcheck
raise GLException(msg)
pyglet.gl.lib.GLException: (0x1282): Invalid operation. The specified operation is not allowed in the current state.
jerkzhang
这是一个自问自答,旨在记录开发经验。
我其实可以直接基于win32的API直接调用操作系统的底层,提前对分辨率问题做好处理,再接入pyglet,很多年前就是采用这种方法的。
但是为了更纯净的代码逻辑与追求,打算完全基于pyglet本身来实现这个事。
pyglet本身是可以实现这个的,设置fullscreen的同时,设定width和height就可以自己去切换到最合适的屏幕物理分辨率模式,去满足这个设定的width和height。
同样的硬件显示器,MacOS下实现是没有问题的;但是Windows下就会报错上面问题。
这个对跨平台开发是不友好的,如果单独要为某个操作系统单独去太多的设置相应的针对性的代码介入,这个就有悖于跨平台开发的初衷了。
因此,在考虑最少介入的方式来解决这个BUG。Debug了这个问题两天,终于找到解决方案。(换了一个AI就解决了,所以,遇到bug换AI产品,会很有帮助)
AI的结论:
这是一个关于 Pyglet 在 Windows 上进行全屏分辨率切换时出现的 OpenGL 状态错误。错误 0x1282 (GL_INVALID_OPERATION) 发生在 glBindVertexArray 调用时,表明 VAO 状态在显示模式切换后失效。
这确实是 Windows 特有的问题,因为 Windows 的显示模式切换(特别是全屏独占模式)会重置 OpenGL 上下文或使其状态失效,而 macOS 使用不同的窗口系统机制避免了这个问题。
根据搜索到的信息,这是一个已知的 Windows 平台特定问题。当 Pyglet 在 Windows 上切换全屏显示模式(特别是改变屏幕分辨率)时,OpenGL 上下文会被重置或失效,导致之前创建的 VAO(Vertex Array Object)ID 变为无效,从而引发 GL_INVALID_OPERATION (0x1282) 错误。
Pyglet 官方文档明确指出:"After toggling fullscreen, the GL context should have retained its state and objects, however the buffers will need to be cleared and redrawn"。但实际上在 Windows 上,显示模式切换会导致 OpenGL 上下文重建,VAO 等对象需要重新创建。
然后给了四种解决方案:
1、使用无边框窗口模拟全屏
import pyglet
from pyglet.window import Window
# 获取屏幕尺寸
display = pyglet.canvas.get_display()
screen = display.get_default_screen()
width, height = screen.width, screen.height
# 创建无边框窗口,覆盖整个屏幕(伪全屏)
# style=Window.WINDOW_STYLE_BORDERLESS 创建没有边框和标题栏的窗口
config = pyglet.gl.Config(double_buffer=True, depth_size=16)
window = Window(
width=width,
height=height,
style=Window.WINDOW_STYLE_BORDERLESS, # 关键:无边框
config=config,
vsync=True
)
# 可选:将窗口置顶
window.set_location(0, 0)确实,目前无边窗口代替全屏是目前PC游戏开发的主流方向。
但是最终这个解决方案,对我目前已有的代码与需求没有任何意义。我是需要借助pyglet来动态调整系统分辨率的。
2、延迟初始化(在窗口显示后创建图形资源)
如果必须使用真实全屏模式,确保在窗口创建并切换到前台后再初始化所有 OpenGL 对象:
import pyglet
from pyglet.window import Window, key
class Game(Window):
def __init__(self):
# 先创建窗口,但不初始化图形资源
display = pyglet.canvas.get_display()
screen = display.get_default_screen()
# 查找合适的全屏模式
target_width, target_height = 1280, 720
mode = None
for m in screen.get_modes():
if m.width == target_width and m.height == target_height:
mode = m
break
config = pyglet.gl.Config(double_buffer=True, depth_size=16)
super().__init__(
fullscreen=True,
screen=screen,
mode=mode, # 指定显示模式
config=config,
vsync=True,
visible=False # 先不显示
)
self.batch = None
self.initialized = False
# 延迟一帧初始化
pyglet.clock.schedule_once(self._delayed_init, 0.1)
def _delayed_init(self, dt):
"""窗口完全创建并激活后,再创建 OpenGL 对象"""
self.switch_to() # 确保上下文当前
# 现在创建 Batch 和图形资源
self.batch = pyglet.graphics.Batch()
# 创建你的精灵、标签等...
self.label = pyglet.text.Label(
'Hello',
batch=self.batch
)
self.initialized = True
self.set_visible(True) # 现在显示窗口
def on_draw(self):
if not self.initialized:
return
self.clear()
self.batch.draw()
if __name__ == '__main__':
game = Game()
pyglet.app.run()这是一个好思路,也是个好习惯,如果我重新开发,我会深度考虑这个方案,但不会完全参照上述代码。但是面对现在debug,不算最佳选项。
3、处理 on_resize 或 on_show 事件重建资源
如果需要在运行时切换全屏/窗口模式,必须监听显示变化事件并重建资源:
class Game(Window):
def __init__(self):
super().__init__(width=800, height=600, resizable=True)
self.batch = None
self._create_resources()
# 监听全屏切换
self._was_fullscreen = self.fullscreen
def _create_resources(self):
"""创建/重新创建所有 OpenGL 资源"""
self.switch_to()
self.batch = pyglet.graphics.Batch()
# 重新创建所有精灵、文本等...
def on_draw(self):
self.clear()
if self.batch:
self.batch.draw()
def on_key_press(self, symbol, modifiers):
if symbol == key.F11:
# 切换全屏前清理
self.batch = None # 释放旧资源
# 切换全屏
self.set_fullscreen(not self.fullscreen)
# 切换后重建资源(延迟一帧确保上下文稳定)
pyglet.clock.schedule_once(self._rebuild_resources, 0.1)
def _rebuild_resources(self, dt):
self._create_resources()方案也许可行,但不纯粹。很绕,为了确保上下稳定,消耗资源去监听变化,介入源代码太深。
4、使用 shadow_window 选项
import pyglet # 或者尝试禁用,看是否有改善 pyglet.options['shadow_window'] = False # 默认即为 True,这里设置成False
对于方案4,AI给的答案并不是肯定,是一种尝试。
但是尝试下来这种方案有用。
也许这种方案未必真正意义上解决了问题,这个还需要实验更多硬件设备或开发者反馈才行。
但综合考虑解决了眼前的BUG和这种方案足够的友好程度,因此,最终选择了该方案,即设置pyglet.options['shadow_window']为False。