142 lines
5.7 KiB
Python
142 lines
5.7 KiB
Python
"""The studio-side three-dimensional skeleton: importable, inert, Qt-safe.
|
|
|
|
Acceptance for stage 0 (``docs/architecture/studio-3d-stages.md``): the module
|
|
tree exists, it can be imported without PyOpenGL, and the one piece that must run
|
|
before ``QApplication`` — the surface format — selects a complete profile.
|
|
|
|
Nothing here renders. These tests are the floor a later stage must keep: the GL
|
|
binding is imported only once a real context exists, and the stubs say which stage
|
|
fills them in.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def restore_surface_format():
|
|
"""The default surface format is process-wide; put it back afterwards."""
|
|
from PyQt5.QtGui import QSurfaceFormat
|
|
|
|
previous = QSurfaceFormat.defaultFormat()
|
|
try:
|
|
yield
|
|
finally:
|
|
QSurfaceFormat.setDefaultFormat(previous)
|
|
|
|
|
|
def test_the_skeleton_does_not_import_a_gl_binding(qt_app):
|
|
"""PyOpenGL must be imported for the platform Qt chose, not at import time.
|
|
|
|
The binding decides GLX versus EGL when it first loads, so importing it while
|
|
merely constructing the module tree would freeze a guess. The test fails if a
|
|
future change adds a module-level ``import OpenGL``.
|
|
"""
|
|
for name in [name for name in sys.modules if name == "OpenGL" or name.startswith("OpenGL.")]:
|
|
del sys.modules[name]
|
|
|
|
import orisys_studio.scene3d.camera # noqa: F401
|
|
import orisys_studio.scene3d.coordinator # noqa: F401
|
|
import orisys_studio.scene3d.gl_backend # noqa: F401
|
|
import orisys_studio.scene3d.panel # noqa: F401
|
|
import orisys_studio.scene3d.widget # noqa: F401
|
|
|
|
loaded = [name for name in sys.modules if name == "OpenGL" or name.startswith("OpenGL.")]
|
|
assert loaded == [], f"importing the skeleton pulled in {loaded}"
|
|
|
|
|
|
def test_configure_surface_format_selects_a_complete_profile(qt_app, restore_surface_format):
|
|
from PyQt5.QtGui import QSurfaceFormat
|
|
|
|
from orisys_studio.scene3d.gl_backend import configure_surface_format
|
|
|
|
configure_surface_format("desktop33")
|
|
desktop = QSurfaceFormat.defaultFormat()
|
|
assert desktop.renderableType() == QSurfaceFormat.OpenGL
|
|
assert (desktop.majorVersion(), desktop.minorVersion()) == (3, 3)
|
|
assert desktop.profile() == QSurfaceFormat.CoreProfile
|
|
assert desktop.depthBufferSize() == 24
|
|
assert desktop.samples() == 0, "v1 does not pay for multisampling"
|
|
|
|
configure_surface_format("gles30")
|
|
embedded = QSurfaceFormat.defaultFormat()
|
|
assert embedded.renderableType() == QSurfaceFormat.OpenGLES
|
|
assert (embedded.majorVersion(), embedded.minorVersion()) == (3, 0)
|
|
|
|
with pytest.raises(ValueError):
|
|
configure_surface_format("vulkan") # type: ignore[arg-type]
|
|
|
|
|
|
def test_the_widget_holds_cpu_state_and_touches_no_context(qt_app):
|
|
"""Constructing the widget must not need a current context or a GPU."""
|
|
from orisys_app_shared.scene3d.contracts import DisplaySettings, FrameIdentity
|
|
from orisys_studio.scene3d.camera import OrbitCamera
|
|
from orisys_studio.scene3d.gl_backend import GLRenderer
|
|
from orisys_studio.scene3d.widget import Sensor3DWidget
|
|
|
|
widget = Sensor3DWidget(GLRenderer(), OrbitCamera())
|
|
try:
|
|
assert isinstance(widget, Sensor3DWidget)
|
|
widget.set_settings(DisplaySettings(display_gain=3.0))
|
|
assert widget._settings.display_gain == 3.0
|
|
# Nothing has been presented yet, and no context was needed to get here.
|
|
assert widget._frame is None
|
|
finally:
|
|
widget.deleteLater()
|
|
|
|
|
|
def test_the_stubs_name_the_stage_that_fills_them_in(qt_app):
|
|
from orisys_app_shared.scene3d.contracts import (
|
|
CameraPreset,
|
|
DisplaySettings,
|
|
FrameIdentity,
|
|
GLCapabilities,
|
|
RenderFrame,
|
|
SceneAssets,
|
|
)
|
|
from orisys_studio.scene3d.camera import OrbitCamera
|
|
from orisys_studio.scene3d.coordinator import ThreeDCoordinator
|
|
from orisys_studio.scene3d.gl_backend import GLDispatch, GLRenderer
|
|
from orisys_studio.scene3d.widget import Sensor3DWidget
|
|
|
|
camera = OrbitCamera()
|
|
renderer = GLRenderer()
|
|
widget = Sensor3DWidget(renderer, camera)
|
|
coordinator = ThreeDCoordinator(widget, composer=object(), executor=None, max_fps=30) # type: ignore[arg-type]
|
|
try:
|
|
assert coordinator.state == "disabled"
|
|
assert coordinator.generation == 0
|
|
assert coordinator.superseded_frames == 0 # the mailbox is real, not a stub
|
|
|
|
with pytest.raises(NotImplementedError) as camera_error:
|
|
camera.set_preset(CameraPreset("default", (0.0, 0.0, 1.0), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0), 0.7))
|
|
assert "stage 1" in str(camera_error.value)
|
|
|
|
with pytest.raises(NotImplementedError) as gl_error:
|
|
GLDispatch().bind_current_context(object())
|
|
assert "stage 1" in str(gl_error.value)
|
|
|
|
with pytest.raises(NotImplementedError) as activate_error:
|
|
coordinator.activate(SceneAssets, "live-A") # type: ignore[arg-type]
|
|
assert "stage 3" in str(activate_error.value)
|
|
|
|
with pytest.raises(NotImplementedError) as settings_error:
|
|
coordinator.set_settings(DisplaySettings())
|
|
assert "stage 3" in str(settings_error.value)
|
|
|
|
with pytest.raises(NotImplementedError) as initialize_error:
|
|
renderer.initialize(GLCapabilities("desktop33", "", "", "", "", "", False, 0))
|
|
assert "stage 1" in str(initialize_error.value)
|
|
|
|
with pytest.raises(NotImplementedError) as update_error:
|
|
renderer.update_surface(None) # type: ignore[arg-type]
|
|
assert "stage 1" in str(update_error.value)
|
|
|
|
assert FrameIdentity("live-A", 1, 0, 0.0, 1).generation == 1
|
|
assert RenderFrame is not None and SceneAssets is not None
|
|
finally:
|
|
widget.deleteLater()
|