feat: Render thumbnails in CLI via offscreen OpenGL (CGL/EGL/WGL)#15355
Open
abdoughnut wants to merge 1 commit into
Open
feat: Render thumbnails in CLI via offscreen OpenGL (CGL/EGL/WGL)#15355abdoughnut wants to merge 1 commit into
abdoughnut wants to merge 1 commit into
Conversation
Closes prusa3d#7878. The CLI accepts --thumbnails and printer-profile thumbnail sizes but silently discards them for non-3MF input: get_thumbnail_generator_cli() returns an empty lambda, and the declared thumbnails never make it into the exported gcode. Fix that by generating the thumbnails in-process using a platform-native offscreen OpenGL context plus a small dedicated renderer. The renderer lives in a new wx-free static library (libslic3r_cli_thumbnails) linked only into the CLI target, so the GUI code path and libslic3r_gui are untouched. The offscreen context is created natively on each platform: - macOS : CGLCreateContext with no drawable, GL 3.2 core - Linux/BSD : EGL; tries EGL_MESA_platform_surfaceless first (via the core eglGetPlatformDisplay entry point with a fallback to eglGetPlatformDisplayEXT for older stacks) and falls back to eglGetDisplay(EGL_DEFAULT_DISPLAY) + a pbuffer surface if any step of the surfaceless path fails, including a probe eglMakeCurrent() to catch drivers that accept the surfaceless display but fail to make it current - Windows : hidden HWND + wglCreateContextAttribsARB OSMesa is deliberately not used: it was removed upstream in Mesa 25.1.0. EGL_MESA_platform_surfaceless makes the Linux/BSD path work on truly headless hosts (CI, Docker, servers with no X/Wayland), addressing the failure mode that limited the earlier EGL-only attempt in prusa3d#15327 to Linux only. The render path reuses the stock shaders/140/gouraud_light shaders and resolves the per-volume colour the same way the GUI does (Plater::get_extruder_colors_from_plater_config): extruder_colour first, filament_colour as fallback. Framing is an isometric ortho projection fit to the scene bounding box in view space, so thumbnails of any aspect (portrait, landscape, square) render the full model without clipping. Output is not byte-for-byte identical to the GUI thumbnail: the CLI path does not render the print bed. export_gcode() requests show_bed=true, so GUI thumbnails include the bed plate while CLI thumbnails show only the model. Rendering the bed in the CLI would require pulling the bed model and its assets through wxWidgets, which would bring wx into the CLI target; that is intentionally out of scope for this change. The CLI logs at debug level when show_bed is requested so the divergence is discoverable. On any failure (no GPU, context creation fails, shader compile fails, FBO incomplete), the renderer logs a warning and returns an empty list, so the CLI behaves exactly as it does today -- no regression path. Verified end-to-end on macOS 26.4 (CGL) and on Ubuntu 24.04 headless in Docker (EGL surfaceless with Mesa llvmpipe, no X or Wayland); both produce pixel-identical output for landscape and portrait thumbnails. The Windows WGL backend is written and CMake-guarded but has not been runtime-tested. There is no Catch2 coverage: the new paths are all GL-dependent and not cheaply mockable; validation here is end-to-end.
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
PrusaSlicer CLI has never embedded thumbnails in
.gcode/.bgcodeoutput for STL/OBJ input.get_thumbnail_generator_cli()insrc/CLI/ProcessActions.cppreturns an empty lambda for non-3MF sources, so--thumbnails/ profile-declared thumbnail sizes are parsed and then discarded. This PR generates the thumbnails for real, in the CLI, using an offscreen OpenGL context on every supported platform — including truly headless Linux/BSD (CI, Docker, servers with no X/Wayland).Closes #7878.
Background
@bubnikv flagged the architectural obstacle in 2022: "thumbnails are generated by the same OpenGL context that the 3D scene is rendered with; there is no OpenGL context available in command line slicer, thus there are no thumbnails." This PR creates that missing context — without linking wxWidgets into the CLI path and without requiring a display server.
A prior attempt (#15327, self-closed) used EGL on Linux only, with explicit stubs for macOS and Windows. Shipping CLI thumbnails on just one platform is harder to justify than shipping on all three, so this PR extends the idea to all three via each platform's native offscreen GL primitive.
Why not OSMesa
OSMesa would have been a clean cross-platform choice historically, but it was removed upstream from Mesa in 25.1.0 (June 2025). Current Homebrew
mesa(26.0.4) ships nolibOSMesa.dylib, and distros are following suit. OrcaSlicer, which previously used OSMesa, removed it in June 2025 for the same reason. So OSMesa is not a viable path for a 2026 merge; each platform's native offscreen primitive is.Architecture
Key design choices:
libslic3r_cli_thumbnailsis added alongsidelibslic3r; it does NOT depend onlibslic3r_gui. The CLI remains wx-free.gouraud_lightuniforms, sameemission_factor=0, sameuniform_colorsemantics, same colour resolution path (Plater::get_extruder_colors_from_plater_config:extruder_colourfirst,filament_colourfallback).export_gcode()requestsshow_bed=true, so GUI thumbnails include the bed plate while CLI thumbnails show only the model. Rendering the bed in the CLI would require pulling the bed model and its assets through wxWidgets, which would bring wx into the CLI target — intentionally out of scope for this change. The CLI logs at debug level whenshow_bedis requested so the divergence is discoverable in user logs.eglMakeCurrent()actually working on the surfaceless display/context, and only then commits. On any failure it falls back toeglGetDisplay(EGL_DEFAULT_DISPLAY)+ a PBuffer. Surfaceless platform lookup trieseglGetPlatformDisplay(EGL 1.5 core) with a fallback toeglGetPlatformDisplayEXT, covering mixed EGL 1.5 / extension-only stacks.Unknown error) logs at warning and returns an empty list. The CLI behaves exactly as it does today — no regression possible.find_package(OpenGL COMPONENTS EGL)suppliesOpenGL::EGL.Files changed
src/libslic3r_cli_thumbnails/OffscreenGLContext.hppsrc/libslic3r_cli_thumbnails/OffscreenGLContext_CGL.cppsrc/libslic3r_cli_thumbnails/OffscreenGLContext_EGL.cppsrc/libslic3r_cli_thumbnails/OffscreenGLContext_WGL.cppsrc/libslic3r_cli_thumbnails/OffscreenGLContext_Stub.cppsrc/libslic3r_cli_thumbnails/CLIThumbnailRenderer.{hpp,cpp}src/libslic3r_cli_thumbnails/CMakeLists.txtsrc/CMakeLists.txtPrusaSlicersrc/CLI/ProcessActions.cpptry/catchPlatform support
EGL_MESA_platform_surfaceless, no X/Wayland)UNIXbranch and the source guard enumerates these platformswglCreateContextAttribsARB)Verification
With stock
tests/data/default_fff.ini+ a bright filament override:./build/src/PrusaSlicer \ --load tests/data/default_fff.ini \ --filament-colour "#FF6600" \ --thumbnails "256x640/PNG,640x480/PNG" \ --export-gcode --output /tmp/bench.gcode \ resources/shapes/3DBenchy.stlBoth macOS (CGL) and Linux (EGL surfaceless in Docker) produce the same output with all thumbnails embedded and decoded; 256×640 portrait renders the full benchy with no horizontal clipping; 640×480 landscape fills the frame tightly.
Testing note
No Catch2 coverage is added. The new code paths are all GL-dependent and there is no cheap mock for the GL function pointer table; the PR's validation is end-to-end (slice a known STL, parse the gcode, inspect thumbnail magic bytes, visually check the decoded image). Mocking the driver to add unit tests is a disproportionate amount of work for the value delivered.
GUI regression check
libslic3r_guiis unmodified.GLCanvas3D::_render_thumbnail_internalis unchanged. The 3MF thumbnail extraction path inProcessActions.cppis unchanged. The new lambda for non-3MF input only differs by dispatching into the new renderer; on any renderer failure it returns an empty list, restoring today's behaviour.Credit / prior art
The design was informed by OrcaSlicer's CLI thumbnail work — the idea of a dedicated CLI render pass independent of the GUI's canvas. OrcaSlicer's current implementation uses a hidden GLFW window and therefore requires a display session; this PR uses native offscreen primitives so it also works on headless Linux, which is where this feature is most often requested.
Closes #7878. Supersedes #15327.