From 5f83000947dc31df84faf779fb03bb420977c30e Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Sun, 24 Aug 2025 17:08:00 +0200 Subject: [PATCH 1/5] Detect and set GL or GLES renderable type automatically Signed-off-by: Nikita Vakula --- main.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/main.cpp b/main.cpp index 8e6804b..63552ea 100644 --- a/main.cpp +++ b/main.cpp @@ -19,6 +19,11 @@ int main(int argc, char **argv) { QSurfaceFormat format; format.setMajorVersion(4); format.setMinorVersion(6); + if (QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGL) { + format.setRenderableType(QSurfaceFormat::OpenGL); + } else { + format.setRenderableType(QSurfaceFormat::OpenGLES); + } QOpenGLContext gl_ctx; gl_ctx.setFormat(format); From a6fdbab28e2c1be85c68c4398af5b7b7083c1fbc Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Sun, 24 Aug 2025 18:51:34 +0200 Subject: [PATCH 2/5] Remove all widget-based code Signed-off-by: Nikita Vakula --- main.cpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/main.cpp b/main.cpp index 63552ea..0bc5045 100644 --- a/main.cpp +++ b/main.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -66,12 +65,6 @@ int main(int argc, char **argv) { auto tg = QQuickRenderTarget::fromOpenGLTexture(fb.texture(), fb.size()); window.setRenderTarget(tg); - // Label that displays the content of the custom framebuffer. - QLabel label; - label.setFixedWidth(fb.size().width()); - label.setFixedHeight(fb.size().height()); - label.show(); - QObject::connect( &control, &QQuickRenderControl::sceneChanged, &control, [&] { @@ -81,8 +74,8 @@ int main(int argc, char **argv) { control.render(); control.endFrame(); - // To simplify, just display the image. - label.setPixmap(QPixmap::fromImage(fb.toImage())); + // To simplify, just save the image. + fb.toImage().save("output.png"); }, Qt::QueuedConnection); From f66cbfc3ccf9504390bb5de5e36c6e0c1dd10246 Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Sun, 24 Aug 2025 18:52:12 +0200 Subject: [PATCH 3/5] Add cli option "output" Signed-off-by: Nikita Vakula --- main.cpp | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/main.cpp b/main.cpp index 0bc5045..bd560e2 100644 --- a/main.cpp +++ b/main.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include #include @@ -13,8 +15,21 @@ #include int main(int argc, char **argv) { - QApplication app{argc, argv}; - + QGuiApplication app(argc, argv); + + QCommandLineParser parser; + parser.setApplicationDescription("Offscreen Qt Quick Renderer"); + parser.addHelpOption(); + QCommandLineOption outputOption({"o", "output"}, "Path to output image", + "output"); + parser.addOption(outputOption); + parser.process(app); + + QString outputPath = parser.value(outputOption); + if (outputPath.isEmpty()) { + qCritical() << "No output path specified. Use --output or -o "; + return 1; + } QSurfaceFormat format; format.setMajorVersion(4); format.setMinorVersion(6); @@ -75,7 +90,7 @@ int main(int argc, char **argv) { control.endFrame(); // To simplify, just save the image. - fb.toImage().save("output.png"); + fb.toImage().save(outputPath); }, Qt::QueuedConnection); From 46afbda76a8c7c36fcf22ee1748554c0f2370f01 Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Sun, 24 Aug 2025 18:33:08 +0200 Subject: [PATCH 4/5] Add container image This image can be used to test headless mode. Signed-off-by: Nikita Vakula --- .dockerignore | 3 +++ CMakeLists.txt | 2 ++ Dockerfile | 21 +++++++++++++++++++++ entrypoint.sh | 13 +++++++++++++ 4 files changed, 39 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6186da2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +build +Dockerfile +output.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 55705ca..5b6a695 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,3 +19,5 @@ target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Quick Qt6::Widgets ) + +install(TARGETS ${PROJECT_NAME} DESTINATION bin) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b9c724 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM ubuntu:latest + +RUN apt update && \ + apt install -y \ + xvfb mesa-utils \ + cmake \ + build-essential \ + qt6-declarative-dev \ + qml6-module-qtquick \ + qml6-module-qtquick-controls \ + qml6-module-qtqml-workerscript \ + qml6-module-qtquick-templates + +WORKDIR /app +COPY . . +RUN cmake -B build -S . +RUN cmake --build build --parallel --target install + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh +CMD ["/entrypoint.sh"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..089680b --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +pkill Xvfb || true + +Xvfb :99 -screen 0 1024x768x24 2>/dev/null & +XVFB_PID=$! +export DISPLAY=:99 +export QT_OPENGL=desktop + +# Trap all common signals to ensure Xvfb is killed +trap "kill $XVFB_PID" EXIT SIGHUP SIGINT SIGTERM SIGQUIT + +QT_QPA_PLATFORM=xcb offscreen_sample -o /output/image.png From 1484f33dfbddff64b3b4a1049e4a6b7c1bf48671 Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Sun, 24 Aug 2025 19:29:20 +0200 Subject: [PATCH 5/5] Adjust readme Signed-off-by: Nikita Vakula --- .github/workflows/build.yml | 19 +++++++++++++++++++ .gitignore | 1 + README.md | 31 +++++++++++++++++++++++++++---- 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..4da943e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,19 @@ +name: Build inside Docker + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build . diff --git a/.gitignore b/.gitignore index 5acb669..5fe7c55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build .vscode +*.png diff --git a/README.md b/README.md index 4c9da61..0923e07 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,36 @@ +# ![status](https://github.com/krjakbrjak/offscreen_rendering/actions/workflows/build.yml/badge.svg) + # Offscreen Rendering -Rendering into a custom framebuffer, also known as off-screen rendering or rendering to a texture, plays a crucial role in numerous scenarios within computer graphics and game development. This repo is just an example that demonstrates how this can be effortlessly achieved using the Qt framework (version 6.6 is used). +Rendering into a custom framebuffer, also known as off-screen rendering or rendering to a texture, plays a crucial role in numerous scenarios within computer graphics and game development. This repo is just an example that demonstrates how this can be achieved using the Qt framework (version 6). -![main.qml](screenshot.png) +![Screenshot of Qt offscreen rendering example](screenshot.png) ## Build and run ```bash cmake -B build -S . cmake --build build --parallel -./build/offscreen_sample -``` \ No newline at end of file +``` + +## Dependencies + +This project uses Qt (Qt Quick, Qt QML, Qt OpenGL). The required Ubuntu packages for Qt and other dependencies are listed in the Dockerfile included in this repository. + +## Usage + +When running inside Docker, the output image will be written to a mounted folder. Specify the output path using the `--output` or `-o` CLI argument: + +```sh +./build/offscreen_sample --output /output/image.png +``` + +Make sure to mount a host directory to `/output` in your Docker container to access the generated image. + +## Example Docker run (headless mode) + +```sh +docker run --rm -u $(id -u):$(id -g) -v $(pwd)/output:/output $(docker build . -q) +``` + +This will save the rendered image to the `output` folder on your host machine.