fix(render): configure the headless GL platform on the colour path#71
Merged
Merged
Conversation
…nder PyrenderBackend.render took pyrender's default GL platform, so it died with NoSuchDisplayException on any box without an X display. The colour path now calls configure_headless() itself, immediately before it imports pyrender -- the same ordering the depth path has used since it was written, because PyOpenGL binds its platform at import time and a later call is a silent no-op. bamdadd asked for this in bamdadd#36: leaving the platform decision to the caller only meant everyone rediscovered the same exception the hard way. configure_headless() no-ops when DISPLAY exists or PYOPENGL_PLATFORM is already exported, so desktops are unchanged and only the CI/server case is fixed. Two tests cover it. test_render_configures_headless_before_importing_pyrender spies both configure_headless and the pyrender import and asserts the order, so it guards the regression without needing the render extra; test_render_headless_smoke_with_no_display renders a real frame with DISPLAY unset in a subprocess, gated on find_spec like the depth smoke. The depth module's docstring note that the colour path deliberately abstains, and the "known issue" section of docs/renderer-eval.md, are updated to match. Fixes bamdadd#36 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Owner
|
Merged, thanks. The subprocess smoke plus the ordering guard is exactly the right way to test this, and I appreciate you writing down why the in-process case still needs a display rather than papering over it. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PyrenderBackend.rendernow callsconfigure_headless()itself, so the colour path works on a display-less box instead of raisingNoSuchDisplayException.Related Issues and Pull Requests
Fixes #36
Changes
src/multicam_sim/dsl/render.py—render()callsconfigure_headless()before reaching_import_pyrender. The import of it is function-local becausedepth.pyimports_build_pyrender_scene/_import_pyrenderfrom this module, so a module-level import is a hard circular one; function-local imports are already this file's idiom (_occluder_mesh).src/multicam_sim/dsl/depth.py— updated the docstring that said the colour path "deliberately does not make" the platform decision, since it now does.docs/renderer-eval.md— same correction; it stated the colour path cannot render headless, which this change overturns.tests/test_render.py— the regression guard you asked for, in two layers.On placement:
_build_pyrender_scenehas exactly two callers,render()andrender_depth(), and the latter has configured the platform since it was written. Configuring inside_build_pyrender_scenewould also be too late for the colour path —render()imports pyrender before calling it, so the call would silently no-op, which is the failureconfigure_headlessexists to prevent.Testing
uv run ruff check .,uv run ruff format --check .(67 files),uv run mypy src(28 files) all clean.uv run pytestwent 246 passed / 3 skipped → 247 passed / 4 skipped, with../multicam-occlusionpinned to the59f4906your CI checks out.The test is in two layers because a real headless render needs system OSMesa:
configure_headlessfires before pyrender is imported, andPYOPENGL_PLATFORMis already in force at the import. It spies both ends and asserts the sequence, so it needs no optional dependency. Reverting the two added lines fails it withassert ['import_pyrender'] == ['configure_headless', 'import_pyrender'].renderextra, matching howtests/test_depth.pygates its own smoke.I used that file's
find_spec+ subprocess idiom rather thanpytest.importorskip, becauseimportorskipimports pyrender in-process and after thatconfigure_headlesscorrectly raisesRuntimeError: PyOpenGL is already imported.This box has no
libosmesa6, so the gated render can't complete here — and your existingtest_render_depth_headless_smokefails identically for the same reason, so I didn't invent a softer skip. I did verify the path end to end twice with EGL substituted for OSMesa, from a process withPYOPENGL_PLATFORMunset:render()→configure_headless()→ env set → pyrender imported → a real(480, 640, 3)uint8 image with 11013 non-zero pixels, no display.Follow-ups / Known Limitations
test_render_produces_an_image_when_pyrender_presentstill can't pass on a bare headless box, but its error changes: previouslyNoSuchDisplayException, nowRuntimeError: PyOpenGL is already imported, so PYOPENGL_PLATFORM='osmesa' would be ignored…. That's a pre-existing limitation rather than a new failure, and arguably a better message since it names the remedy — but converting it to a subprocess would duplicate the new smoke, so I left it. Happy to convert it if you'd prefer.Footer
Generated by Claude Opus 5 (1M context) (brief, implementation, review)