Skip to content

Commit 52132d7

Browse files
committed
Headless rendering support
1 parent 9b3b6ab commit 52132d7

File tree

7 files changed

+122
-39
lines changed

7 files changed

+122
-39
lines changed

demosys/context/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import moderngl
2-
from .base import Window
32

43
# Window instance shortcut
54
WINDOW = None # noqa
65

76

8-
def window() -> Window:
7+
def window() -> 'demosys.context.base.Window':
98
"""The window instance we are rendering to"""
109
return WINDOW
1110

demosys/context/base.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import moderngl as mgl
55
from demosys.conf import settings
6+
from demosys.opengl.fbo import WindowFBO
7+
from demosys import context
68

79
GLVersion = namedtuple('GLVersion', ['major', 'minor'])
810

@@ -12,10 +14,8 @@ class Window:
1214
def __init__(self):
1315
"""
1416
Base window intializer
15-
16-
:param width: window width
17-
:param height: window height
1817
"""
18+
self.frames = 0
1919
self.width = settings.WINDOW['size'][0]
2020
self.height = settings.WINDOW['size'][1]
2121

@@ -26,6 +26,7 @@ def __init__(self):
2626
self.sys_camera = None
2727
self.timer = None
2828
self.resources = None
29+
self.manager = None
2930

3031
self.gl_version = GLVersion(*settings.OPENGL['version'])
3132
self.resizable = settings.WINDOW.get('resizable') or False
@@ -37,26 +38,39 @@ def __init__(self):
3738
self.vsync = settings.WINDOW.get('vsync')
3839
self.cursor = settings.WINDOW.get('cursor')
3940

41+
self._calc_viewport()
42+
4043
# ModernGL context
4144
self.ctx = None
4245

46+
WindowFBO.window = self
47+
self.fbo = WindowFBO
48+
context.WINDOW = self
49+
50+
def draw(self, current_time, frame_time):
51+
self.manager.draw(current_time, frame_time, WindowFBO)
52+
4353
def clear(self):
4454
"""Clear the scren"""
4555
self.ctx.clear(
4656
red=0.0, blue=0.0, green=0.0, alpha=0.0, depth=1.0,
47-
viewport=(0, 0, self.buffer_width, self.buffer_height)
57+
viewport=self._viewport,
4858
)
4959

60+
def use(self):
61+
"""Render to this window"""
62+
raise NotImplementedError()
63+
5064
def viewport(self):
51-
self.fbo.use()
65+
self.ctx.viewport = self._viewport
5266

5367
def swap_buffers(self):
5468
"""Swap frame buffer"""
5569
raise NotImplementedError()
5670

5771
def resize(self, width, height):
5872
"""Resize window"""
59-
raise NotImplementedError()
73+
self._calc_viewport()
6074

6175
def close(self):
6276
"""Set the close state"""
@@ -70,6 +84,10 @@ def terminate(self):
7084
"""Cleanup after close"""
7185
raise NotImplementedError()
7286

87+
def mgl_fbo(self):
88+
"""Returns the ModernGL fbo used by this window"""
89+
raise NotImplementedError()
90+
7391
def print_context_info(self):
7492
"""Prints out context info"""
7593
print("Context Version:")
@@ -80,3 +98,12 @@ def print_context_info(self):
8098
print('python:', sys.version)
8199
print('platform:', sys.platform)
82100
print('code:', self.ctx.version_code)
101+
102+
def _calc_viewport(self):
103+
"""Calculate viewport with correct aspect ratio"""
104+
# The expected height with the current viewport width
105+
expected_height = int(self.buffer_width / self.aspect_ratio)
106+
107+
# How much positive or negative y padding
108+
blank_space = self.buffer_height - expected_height
109+
self._viewport = (0, blank_space // 2, self.buffer_width, expected_height)

demosys/context/glfw.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def __init__(self):
5050
self.buffer_width, self.buffer_height = glfw.get_framebuffer_size(self.window)
5151
print("Frame buffer size:", self.buffer_width, self.buffer_height)
5252
print("Actual window size:", glfw.get_window_size(self.window))
53+
self._calc_viewport()
5354

5455
glfw.make_context_current(self.window)
5556

@@ -60,18 +61,22 @@ def __init__(self):
6061

6162
glfw.set_key_callback(self.window, self.key_event_callback)
6263
glfw.set_cursor_pos_callback(self.window, self.mouse_event_callback)
63-
# glfw.set_window_size_callback(self.window, window_resize_callback)
64+
glfw.set_window_size_callback(self.window, self.window_resize_callback)
6465

6566
# Create mederngl context from existing context
6667
self.ctx = mgl.create_context()
6768

69+
def use(self):
70+
self.ctx.screen.use()
71+
6872
def should_close(self):
6973
return glfw.window_should_close(self.window)
7074

7175
def close(self):
7276
glfw.set_window_should_close(self.window, True)
7377

7478
def swap_buffers(self):
79+
self.frames += 1
7580
glfw.swap_buffers(self.window)
7681
self.poll_events()
7782

@@ -80,10 +85,14 @@ def resize(self, width, height):
8085
self.height = height
8186
self.buffer_width, self.buffer_height = glfw.get_framebuffer_size(self.window)
8287
print("Resize:", self.width, self.height, self.buffer_width, self.buffer_height)
88+
super().resize(width, height)
8389

8490
def terminate(self):
8591
glfw.terminate()
8692

93+
def mgl_fbo(self):
94+
return self.ctx.screen
95+
8796
def poll_events(self):
8897
"""Poll events from glfw"""
8998
glfw.poll_events()

demosys/context/headless.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import moderngl
2+
from demosys.conf import settings
3+
from demosys.conf import ImproperlyConfigured
4+
from demosys.opengl.fbo import FBO
5+
6+
from .base import Window
7+
8+
9+
class HeadlessWindow(Window):
10+
11+
def __init__(self):
12+
super().__init__()
13+
14+
self.headless_frames = getattr(settings, 'HEADLESS_FRAMES', 0)
15+
self.headless_duration = getattr(settings, 'HEADLESS_DURATION', 0)
16+
17+
if not self.headless_frames and not self.headless_duration:
18+
raise ImproperlyConfigured("HEADLESS_DURATION or HEADLESS_FRAMES not present in settings")
19+
20+
self._close = False
21+
self.ctx = moderngl.create_standalone_context()
22+
self.screenbuffer = FBO.create((self.width, self.height), depth=True)
23+
24+
def draw(self, current_time, frame_time):
25+
super().draw(current_time, frame_time)
26+
27+
if self.headless_duration and current_time >= self.headless_duration:
28+
self.close()
29+
30+
def use(self):
31+
self.screenbuffer.use(stack=False)
32+
33+
def should_close(self):
34+
return self._close
35+
36+
def close(self):
37+
self._close = True
38+
39+
def resize(self, width, height):
40+
pass
41+
42+
def swap_buffers(self):
43+
self.frames += 1
44+
45+
if self.headless_frames and self.frames >= self.headless_frames:
46+
self.close()
47+
48+
def terminate(self):
49+
pass
50+
51+
def mgl_fbo(self):
52+
return self.screenbuffer.mglo

demosys/opengl/fbo.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,8 @@ class WindowFBO:
1010
@classmethod
1111
def use(cls):
1212
"""Sets the viewport back to the buffer size of the screen/window"""
13-
# The expected height with the current viewport width
14-
expected_height = int(cls.window.buffer_width / cls.window.aspect_ratio)
15-
16-
# How much positive or negative y padding
17-
blank_space = cls.window.buffer_height - expected_height
18-
19-
cls.window.ctx.screen.use()
20-
cls.window.ctx.viewport = (0, blank_space // 2, cls.window.buffer_width, expected_height)
13+
cls.window.use()
14+
cls.window.viewport()
2115

2216
@classmethod
2317
def release(cls):
@@ -27,7 +21,13 @@ def release(cls):
2721
@classmethod
2822
def clear(cls, red=0.0, green=0.0, blue=0.0, depth=1.0, viewport=None):
2923
"""Dummy clear method"""
30-
cls.ctx.screen.clear(red=red, green=green, blue=blue, depth=depth, viewport=viewport)
24+
cls.window.clear()
25+
26+
@property
27+
@classmethod
28+
def mglo(cls):
29+
"""Internal ModernGL fbo"""
30+
return cls.window.mgl_fbo()
3131

3232

3333
class FBO:
@@ -224,6 +224,10 @@ def __repr__(self):
224224
self.depth_buffer,
225225
)
226226

227+
@property
228+
def mglo(self):
229+
"""Internal ModernGL fbo"""
230+
return self.fbo
227231

228232
class FBOError(Exception):
229233
"""Generic FBO Error"""

demosys/view/controller.py

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
88
import OpenGL
99
OpenGL.ERROR_CHECKING = False # noqa
1010

11-
from demosys import context, resources
11+
from demosys import resources
1212
from demosys.conf import settings
1313
from demosys.effects.registry import Effect
14-
from demosys.opengl.fbo import WindowFBO
1514
from demosys.scene import camera
1615
from demosys.utils import module_loading
1716

@@ -26,24 +25,19 @@ def run(manager=None):
2625
print("window class", window_cls_name)
2726
window_cls = module_loading.import_string(window_cls_name)
2827
window = window_cls()
29-
30-
window.manager = manager
31-
context.WINDOW = window
3228
window.print_context_info()
33-
34-
WindowFBO.window = window
35-
window.fbo = WindowFBO
29+
window.manager = manager
3630

3731
print("Loader started at", time.time())
3832

3933
# Inject attributes into the base Effect class
40-
setattr(Effect, '_window_width', context.WINDOW.buffer_width)
41-
setattr(Effect, '_window_height', context.WINDOW.buffer_height)
42-
setattr(Effect, '_window_aspect', context.WINDOW.aspect_ratio)
43-
setattr(Effect, '_ctx', context.ctx())
34+
setattr(Effect, '_window_width', window.buffer_width)
35+
setattr(Effect, '_window_height', window.buffer_height)
36+
setattr(Effect, '_window_aspect', window.aspect_ratio)
37+
setattr(Effect, '_ctx', window.ctx)
4438

4539
# Set up the default system camera
46-
window.sys_camera = camera.SystemCamera(aspect=context.WINDOW.aspect_ratio, fov=60.0, near=1, far=1000)
40+
window.sys_camera = camera.SystemCamera(aspect=window.aspect_ratio, fov=60.0, near=1, far=1000)
4741
setattr(Effect, '_sys_camera', window.sys_camera)
4842

4943
# Initialize Effects
@@ -66,22 +60,20 @@ def run(manager=None):
6660
window.timer.start()
6761

6862
# Main loop
69-
frames, frame_time = 0, 60.0 / 1000.0
63+
frame_time = 60.0 / 1000.0
7064
# time_start = glfw.get_time()
7165
time_start = time.time()
7266
prev_time = window.timer.get_time()
7367

7468
while not window.should_close():
7569
current_time = window.timer.get_time()
7670

71+
window.use()
7772
window.viewport()
7873
window.clear()
79-
80-
manager.draw(current_time, frame_time, WindowFBO)
81-
74+
window.draw(current_time, frame_time)
8275
window.swap_buffers()
8376

84-
frames += 1
8577
frame_time = current_time - prev_time
8678
prev_time = current_time
8779

@@ -91,6 +83,6 @@ def run(manager=None):
9183
window.terminate()
9284

9385
if duration > 0:
94-
fps = round(frames / duration, 2)
95-
print("Duration: {}s rendering {} frames at {} fps".format(duration, frames, fps))
86+
fps = round(window.frames / duration, 2)
87+
print("Duration: {}s rendering {} frames at {} fps".format(duration, window.frames, fps))
9688
print("Timeline duration:", duration_timer)

demosys/view/screenshot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def create(file_format='png', name=None):
2828
print("SCREENSHOT_PATH not defined in settings. Using cwd as fallback.")
2929

3030
if not Config.target:
31-
Config.target = context.ctx().screen
31+
Config.target = context.window().mgl_fbo()
3232

3333
image = Image.frombytes(
3434
"RGB",

0 commit comments

Comments
 (0)