Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PyQt6 & OpenGL Headless #338

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions extras/examples/opengl_qt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import sys

from PyQt6.QtGui import QSurfaceFormat
from PyQt6.QtOpenGLWidgets import QOpenGLWidget
from PyQt6.QtWidgets import QMainWindow, QApplication
from PyQt6.QtCore import Qt, QSize

from OpenGL.GL import (glPixelZoom)

from pyboy import PyBoy
from pyboy.utils import WindowEvent

ROWS, COLS = 144, 160

if len(sys.argv) > 1:
filename = sys.argv[1]
else:
print("Usage: python opengl_qt.py [ROM file]")
exit(1)


class PyBoyOpenGL(QOpenGLWidget):
def __init__(self, pyboy: PyBoy, parent=None):
super().__init__(parent=parent)
self.pyboy = pyboy
self.setFormat(QSurfaceFormat.defaultFormat())
self.setUpdatesEnabled(True)

self.update()

def paintGL(self):
self.pyboy.tick()
self.update()

def resizeGL(self, width, height):
glPixelZoom(width / COLS, height / ROWS)


class PyBoyWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.pyboy = PyBoy(filename, window="OpenGLHeadless")

self.game_widget = PyBoyOpenGL(self.pyboy)

self.setCentralWidget(self.game_widget)
self.resize(800, 600)
self.setWindowTitle("PyBoy OpenGL")

self.show()

def keyPressEvent(self, event):
"""Handle key press events"""
key = event.key()
if key == Qt.Key.Key_A:
self.pyboy.events.append(WindowEvent(WindowEvent.PRESS_BUTTON_A))
elif key == Qt.Key.Key_S:
self.pyboy.events.append(WindowEvent(WindowEvent.PRESS_BUTTON_B))
elif key == Qt.Key.Key_Up:
self.pyboy.events.append(WindowEvent(WindowEvent.PRESS_ARROW_UP))
elif key == Qt.Key.Key_Down:
self.pyboy.events.append(WindowEvent(WindowEvent.PRESS_ARROW_DOWN))
elif key == Qt.Key.Key_Left:
self.pyboy.events.append(WindowEvent(WindowEvent.PRESS_ARROW_LEFT))
elif key == Qt.Key.Key_Right:
self.pyboy.events.append(WindowEvent(WindowEvent.PRESS_ARROW_RIGHT))
elif key == Qt.Key.Key_Z:
self.pyboy.events.append(WindowEvent(WindowEvent.STATE_SAVE))
elif key == Qt.Key.Key_X:
self.pyboy.events.append(WindowEvent(WindowEvent.STATE_LOAD))
elif key == Qt.Key.Key_Return:
self.pyboy.events.append(WindowEvent(WindowEvent.PRESS_BUTTON_START))
elif key == Qt.Key.Key_Backspace:
self.pyboy.events.append(WindowEvent(WindowEvent.PRESS_BUTTON_SELECT))
elif key == Qt.Key.Key_Space:
self.pyboy.events.append(WindowEvent(WindowEvent.PRESS_SPEED_UP))
elif key == Qt.Key.Key_Escape:
self.pyboy.events.append(WindowEvent(WindowEvent.QUIT))

def keyReleaseEvent(self, event):
"""Handle key release events"""
key = event.key()
if key == Qt.Key.Key_A:
self.pyboy.events.append(WindowEvent(WindowEvent.RELEASE_BUTTON_A))
elif key == Qt.Key.Key_S:
self.pyboy.events.append(WindowEvent(WindowEvent.RELEASE_BUTTON_B))
elif key == Qt.Key.Key_Up:
self.pyboy.events.append(WindowEvent(WindowEvent.RELEASE_ARROW_UP))
elif key == Qt.Key.Key_Down:
self.pyboy.events.append(WindowEvent(WindowEvent.RELEASE_ARROW_DOWN))
elif key == Qt.Key.Key_Left:
self.pyboy.events.append(WindowEvent(WindowEvent.RELEASE_ARROW_LEFT))
elif key == Qt.Key.Key_Right:
self.pyboy.events.append(WindowEvent(WindowEvent.RELEASE_ARROW_RIGHT))
elif key == Qt.Key.Key_Space:
self.pyboy.events.append(WindowEvent(WindowEvent.RELEASE_SPEED_UP))
elif key == Qt.Key.Key_Backspace:
self.pyboy.events.append(WindowEvent(WindowEvent.RELEASE_BUTTON_SELECT))
elif key == Qt.Key.Key_Return:
self.pyboy.events.append(WindowEvent(WindowEvent.RELEASE_BUTTON_START))

def resizeEvent(self, event):
# Resize the game widget while keeping the aspect ratio
size = event.size()
width, height = size.width(), size.height()
aspect_ratio = COLS / ROWS
if width / height > aspect_ratio:
width = height * aspect_ratio
else:
height = width / aspect_ratio
self.game_widget.resize(QSize(int(width), int(height)))
self.game_widget.move(int((size.width() - width) / 2), int((size.height() - height) / 2))


if __name__ == "__main__":
app = QApplication([])
window = PyBoyWindow()
sys.exit(app.exec())
3 changes: 3 additions & 0 deletions pyboy/plugins/manager.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ from pyboy.plugins.base_plugin cimport PyBoyGameWrapper
# imports
from pyboy.plugins.window_sdl2 cimport WindowSDL2
from pyboy.plugins.window_open_gl cimport WindowOpenGL
from pyboy.plugins.window_open_gl_headless cimport WindowOpenGLHeadless
from pyboy.plugins.window_null cimport WindowNull
from pyboy.plugins.debug cimport Debug
from pyboy.plugins.disable_input cimport DisableInput
Expand All @@ -34,6 +35,7 @@ cdef class PluginManager:
# plugin_cdef
cdef public WindowSDL2 window_sdl2
cdef public WindowOpenGL window_open_gl
cdef public WindowOpenGLHeadless window_open_gl_headless
cdef public WindowNull window_null
cdef public Debug debug
cdef public DisableInput disable_input
Expand All @@ -50,6 +52,7 @@ cdef class PluginManager:
cdef public GameWrapperPokemonPinball game_wrapper_pokemon_pinball
cdef bint window_sdl2_enabled
cdef bint window_open_gl_enabled
cdef bint window_open_gl_headless_enabled
cdef bint window_null_enabled
cdef bint debug_enabled
cdef bint disable_input_enabled
Expand Down
16 changes: 16 additions & 0 deletions pyboy/plugins/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# imports
from pyboy.plugins.window_sdl2 import WindowSDL2 # isort:skip
from pyboy.plugins.window_open_gl import WindowOpenGL # isort:skip
from pyboy.plugins.window_open_gl_headless import WindowOpenGLHeadless # isort:skip
from pyboy.plugins.window_null import WindowNull # isort:skip
from pyboy.plugins.debug import Debug # isort:skip
from pyboy.plugins.disable_input import DisableInput # isort:skip
Expand All @@ -29,6 +30,7 @@ def parser_arguments():
# yield_plugins
yield WindowSDL2.argv
yield WindowOpenGL.argv
yield WindowOpenGLHeadless.argv
yield WindowNull.argv
yield Debug.argv
yield DisableInput.argv
Expand Down Expand Up @@ -58,6 +60,8 @@ def __init__(self, pyboy, mb, pyboy_argv):
self.window_sdl2_enabled = self.window_sdl2.enabled()
self.window_open_gl = WindowOpenGL(pyboy, mb, pyboy_argv)
self.window_open_gl_enabled = self.window_open_gl.enabled()
self.window_open_gl_headless = WindowOpenGLHeadless(pyboy, mb, pyboy_argv)
self.window_open_gl_headless_enabled = self.window_open_gl_headless.enabled()
self.window_null = WindowNull(pyboy, mb, pyboy_argv)
self.window_null_enabled = self.window_null.enabled()
self.debug = Debug(pyboy, mb, pyboy_argv)
Expand Down Expand Up @@ -105,6 +109,8 @@ def handle_events(self, events):
events = self.window_sdl2.handle_events(events)
if self.window_open_gl_enabled:
events = self.window_open_gl.handle_events(events)
if self.window_open_gl_headless_enabled:
events = self.window_open_gl_headless.handle_events(events)
if self.window_null_enabled:
events = self.window_null.handle_events(events)
if self.debug_enabled:
Expand Down Expand Up @@ -191,6 +197,8 @@ def _post_tick_windows(self):
self.window_sdl2.post_tick()
if self.window_open_gl_enabled:
self.window_open_gl.post_tick()
if self.window_open_gl_headless_enabled:
self.window_open_gl_headless.post_tick()
if self.window_null_enabled:
self.window_null.post_tick()
if self.debug_enabled:
Expand All @@ -208,6 +216,9 @@ def frame_limiter(self, speed):
if self.window_open_gl_enabled:
done = self.window_open_gl.frame_limiter(speed)
if done: return
if self.window_open_gl_headless_enabled:
done = self.window_open_gl_headless.frame_limiter(speed)
if done: return
if self.window_null_enabled:
done = self.window_null.frame_limiter(speed)
if done: return
Expand All @@ -223,6 +234,9 @@ def window_title(self):
title += self.window_sdl2.window_title()
if self.window_open_gl_enabled:
title += self.window_open_gl.window_title()
# Not sure if this is needed
# if self.window_open_gl_headless_enabled:
# title += self.window_open_gl_headless.window_title()
if self.window_null_enabled:
title += self.window_null.window_title()
if self.debug_enabled:
Expand Down Expand Up @@ -262,6 +276,8 @@ def stop(self):
self.window_sdl2.stop()
if self.window_open_gl_enabled:
self.window_open_gl.stop()
if self.window_open_gl_headless_enabled:
self.window_open_gl_headless.stop()
if self.window_null_enabled:
self.window_null.stop()
if self.debug_enabled:
Expand Down
32 changes: 32 additions & 0 deletions pyboy/plugins/window_open_gl_headless.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy
#

import cython
cimport cython
from libc.stdint cimport uint8_t, uint16_t, uint32_t

from pyboy.logging.logging cimport Logger
from pyboy.plugins.base_plugin cimport PyBoyWindowPlugin

cimport cython
from libc.stdint cimport int64_t, uint8_t, uint16_t, uint32_t

from pyboy.plugins.base_plugin cimport PyBoyWindowPlugin

cdef Logger logger

cdef int ROWS, COLS

cdef class WindowOpenGLHeadless(PyBoyWindowPlugin):
cdef list events

cdef int64_t _ftime
cdef void _glkeyboard(self, str, int, int, bint) noexcept
cdef void _glkeyboardspecial(self, char, int, int, bint) noexcept

# TODO: Callbacks don't really work, when Cythonized
cpdef void _gldraw(self) noexcept
@cython.locals(scale=double)
cpdef void _glreshape(self, int, int) noexcept
102 changes: 102 additions & 0 deletions pyboy/plugins/window_open_gl_headless.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#
# License: See LICENSE.md file
# GitHub: https://github.com/Baekalfen/PyBoy
#

import time

import numpy as np

import pyboy
from pyboy import utils
from pyboy.plugins.base_plugin import PyBoyWindowPlugin
from pyboy.utils import WindowEvent

logger = pyboy.logging.get_logger(__name__)

try:
from OpenGL.GL import (GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, glClear,
glDrawPixels, glFlush, glPixelZoom)
opengl_enabled = True
except (ImportError, AttributeError):
opengl_enabled = False

ROWS, COLS = 144, 160


class WindowOpenGLHeadless(PyBoyWindowPlugin):
def __init__(self, pyboy, mb, pyboy_argv):
super().__init__(pyboy, mb, pyboy_argv)

if not self.enabled():
return

self.events = []

self._ftime = time.perf_counter_ns()

# Cython does not cooperate with lambdas
def _key(self, c, x, y):
self._glkeyboard(c.decode("ascii"), x, y, False)

def _keyUp(self, c, x, y):
self._glkeyboard(c.decode("ascii"), x, y, True)

def _spec(self, c, x, y):
self._glkeyboardspecial(c, x, y, False)

def _specUp(self, c, x, y):
self._glkeyboardspecial(c, x, y, True)

def set_title(self, title):
pass

def handle_events(self, events):
events += self.events
self.events = []
return events

def _glkeyboardspecial(self, c, x, y, up):
# Keybindings should be handled by your own code
# EG: pyboy.events.append(WindowEvent.PRESS_ARROW_UP)
pass

def _glkeyboard(self, c, x, y, up):
# Keybindings should be handled by your own code
# EG: pyboy.events.append(WindowEvent.PRESS_ARROW_UP)
pass

def _glreshape(self, width, height):
scale = max(min(height / ROWS, width / COLS), 1)
self._scaledresolution = (round(scale * COLS), round(scale * ROWS))
glPixelZoom(scale, scale)

def _gldraw(self):
buf = np.asarray(self.renderer._screenbuffer)[::-1, :]
glDrawPixels(COLS, ROWS, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, buf)
glFlush()

def frame_limiter(self, speed):
self._ftime += int((1.0 / (60.0*speed)) * 1_000_000_000)
now = time.perf_counter_ns()
if (self._ftime > now):
delay = (self._ftime - now) // 1_000_000
time.sleep(delay / 1000)
else:
self._ftime = now
return True

def enabled(self):
if self.pyboy_argv.get("window") == "OpenGLHeadless":
if opengl_enabled:
return True
else:
logger.error("Missing depencency \"PyOpenGL\". OpenGL window disabled")
return False

def post_tick(self):
self._gldraw()

def stop(self):
pass

4 changes: 2 additions & 2 deletions pyboy/pyboy.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ def __init__(
)
window = kwargs.pop("window_type")

if window not in ["SDL2", "OpenGL", "null", "headless", "dummy"]:
raise KeyError(f'Unknown window type: {window}. Use "SDL2", "OpenGL", or "null"')
if window not in ["SDL2", "OpenGL", "OpenGLHeadless", "null", "headless", "dummy"]:
raise KeyError(f'Unknown window type: {window}. Use "SDL2", "OpenGL", "OpenGLHeadless" or "null"')

kwargs["window"] = window
kwargs["scale"] = scale
Expand Down