Skip to content

feat: Render thumbnails in CLI via offscreen OpenGL (CGL/EGL/WGL)#15355

Open
abdoughnut wants to merge 1 commit into
prusa3d:masterfrom
abdoughnut:fix/cli-thumbnails-7878
Open

feat: Render thumbnails in CLI via offscreen OpenGL (CGL/EGL/WGL)#15355
abdoughnut wants to merge 1 commit into
prusa3d:masterfrom
abdoughnut:fix/cli-thumbnails-7878

Conversation

@abdoughnut

Copy link
Copy Markdown

Summary

PrusaSlicer CLI has never embedded thumbnails in .gcode / .bgcode output for STL/OBJ input. get_thumbnail_generator_cli() in src/CLI/ProcessActions.cpp returns 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 no libOSMesa.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

Before:
  CLI/ProcessActions::get_thumbnail_generator_cli  ->  []() { return {}; }

After (for STL/OBJ; 3MF path unchanged):
  CLI/ProcessActions  ->  Slic3r::CLIThumbnails::render_thumbnails(model, config, params, resources_dir)
                              |
                              +--> OffscreenGLContext::create()   // platform-dispatched
                              |     macOS     : CGLCreateContext (no drawable)
                              |     Linux/BSD : EGL; EGL_MESA_platform_surfaceless (core +
                              |                 EXT lookup) -> make-current probe ->
                              |                 EGL_DEFAULT_DISPLAY + PBuffer on any failure
                              |     Windows   : hidden HWND + wglCreateContextAttribsARB
                              |
                              +--> compile shaders/140/gouraud_light.{vs,fs} -- same shaders the GUI uses
                              +--> FBO(RGBA8 + DEPTH24) at max requested size
                              +--> iso Ortho camera; view-space bbox fit so any thumbnail aspect covers
                              |    the full model without clipping
                              +--> per-volume mesh upload (smooth normals) + draw, coloured via
                              |    extruder_colour/filament_colour (same logic as GUI's Plater)
                              +--> glReadPixels into ThumbnailData (bottom-up, matches GUI convention)

Key design choices:

  • No wxWidgets in the CLI thumbnail path. A new static library libslic3r_cli_thumbnails is added alongside libslic3r; it does NOT depend on libslic3r_gui. The CLI remains wx-free.
  • Same shaders as the GUI. Same gouraud_light uniforms, same emission_factor=0, same uniform_color semantics, same colour resolution path (Plater::get_extruder_colors_from_plater_config: extruder_colour first, filament_colour fallback).
  • Not pixel-identical to the GUI thumbnail. The CLI 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 — intentionally out of scope for this change. The CLI logs at debug level when show_bed is requested so the divergence is discoverable in user logs.
  • Robust EGL fallback on Linux/BSD. The factory probes three things in order: surfaceless platform availability, eglMakeCurrent() actually working on the surfaceless display/context, and only then commits. On any failure it falls back to eglGetDisplay(EGL_DEFAULT_DISPLAY) + a PBuffer. Surfaceless platform lookup tries eglGetPlatformDisplay (EGL 1.5 core) with a fallback to eglGetPlatformDisplayEXT, covering mixed EGL 1.5 / extension-only stacks.
  • Any-aspect projection fit. The ortho frustum is fit against the scene bounding box projected into view space, not against a bounding sphere scaled by aspect. Portrait, landscape, and square thumbnails all render the full model without clipping.
  • Graceful fallback. Every failure path (no GPU, context create fails, shader compile fails, FBO incomplete, GLEW returns Unknown error) logs at warning and returns an empty list. The CLI behaves exactly as it does today — no regression possible.
  • No new third-party deps. OpenGL and GLEW are already linked for the GUI; we just add them to the new static library. On Linux, find_package(OpenGL COMPONENTS EGL) supplies OpenGL::EGL.

Files changed

File Change
src/libslic3r_cli_thumbnails/OffscreenGLContext.hpp New: platform-neutral interface
src/libslic3r_cli_thumbnails/OffscreenGLContext_CGL.cpp New: macOS backend
src/libslic3r_cli_thumbnails/OffscreenGLContext_EGL.cpp New: Linux/BSD backend
src/libslic3r_cli_thumbnails/OffscreenGLContext_WGL.cpp New: Windows backend
src/libslic3r_cli_thumbnails/OffscreenGLContext_Stub.cpp New: fallback so CLI never fails to link
src/libslic3r_cli_thumbnails/CLIThumbnailRenderer.{hpp,cpp} New: the renderer
src/libslic3r_cli_thumbnails/CMakeLists.txt New: static lib target, conditional platform link
src/CMakeLists.txt Register new subdir; link to PrusaSlicer
src/CLI/ProcessActions.cpp Replace empty lambda with renderer call, with try/catch

Platform support

Platform Backend Build Runtime Evidence
macOS 26.4 (Apple Silicon) CGL, no drawable 3DBenchy renders at landscape and portrait aspects; QOI + PNG magic bytes verified.
Linux Ubuntu 24.04, headless Docker (Mesa llvmpipe, EGL_MESA_platform_surfaceless, no X/Wayland) EGL Pixel-identical to macOS output at both aspects.
Linux with display (native distro install) EGL same code path as above, not re-tested
FreeBSD / OpenBSD / NetBSD / DragonFly EGL not tested; CMake selects EGL on UNIX branch and the source guard enumerates these platforms
Windows WGL (hidden window + wglCreateContextAttribsARB) Code written and CMake guarded, not runtime-tested Reviewer with a Windows box needed.

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.stl

Both 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_gui is unmodified. GLCanvas3D::_render_thumbnail_internal is unchanged. The 3MF thumbnail extraction path in ProcessActions.cpp is 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.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Using CLI to generate gcode files does not create embedded thumbnails

1 participant