diff --git a/src/ifcviewer-minimal/MinimalWindow.cpp b/src/ifcviewer-minimal/MinimalWindow.cpp index 3775637348..e74b3f651e 100644 --- a/src/ifcviewer-minimal/MinimalWindow.cpp +++ b/src/ifcviewer-minimal/MinimalWindow.cpp @@ -127,8 +127,13 @@ void MinimalWindow::setPendingBenchmark(int frames) { pending_benchmark_ = frames; } +void MinimalWindow::setPendingScreenshot(const QString& path) { + pending_screenshot_ = path; +} + void MinimalWindow::applyPendingBenchmark() { - if (pending_camera_.isEmpty() && pending_benchmark_ <= 0) return; + if (pending_camera_.isEmpty() && pending_benchmark_ <= 0 + && pending_screenshot_.isEmpty()) return; if (!pending_camera_.isEmpty()) { QStringList parts = pending_camera_.split(','); @@ -148,4 +153,9 @@ void MinimalWindow::applyPendingBenchmark() { viewport_->setBenchmarkFrames(pending_benchmark_); pending_benchmark_ = 0; } + + if (!pending_screenshot_.isEmpty()) { + viewport_->captureNextFrameToPng(pending_screenshot_, /*quit_after=*/true); + pending_screenshot_.clear(); + } } diff --git a/src/ifcviewer-minimal/MinimalWindow.h b/src/ifcviewer-minimal/MinimalWindow.h index a70ed04144..279a2cdd1f 100644 --- a/src/ifcviewer-minimal/MinimalWindow.h +++ b/src/ifcviewer-minimal/MinimalWindow.h @@ -35,6 +35,10 @@ public: void addFiles(const QStringList& paths); void setPendingCamera(const QString& params); void setPendingBenchmark(int frames); + // One-shot framebuffer capture queued for the next render. Forwarded + // to ViewportWindow::captureNextFrameToPng; the viewport handles the + // glReadPixels + PNG save + optional QCoreApplication::quit. + void setPendingScreenshot(const QString& path); private slots: void onLoadStarted(uint32_t mid, QString display_name); @@ -55,6 +59,7 @@ private: QString pending_camera_; int pending_benchmark_ = 0; + QString pending_screenshot_; }; #endif // MINIMALWINDOW_H diff --git a/src/ifcviewer-minimal/main.cpp b/src/ifcviewer-minimal/main.cpp index 75db1b212b..46fa62e1ca 100644 --- a/src/ifcviewer-minimal/main.cpp +++ b/src/ifcviewer-minimal/main.cpp @@ -44,6 +44,8 @@ int main(int argc, char* argv[]) { "Set camera: tx,ty,tz,dist,yaw,pitch", "params"}); parser.addOption({{"b", "benchmark"}, "Run N frames then print stats and exit", "frames"}); + parser.addOption({{"s", "screenshot"}, + "Render one frame after load, save to PATH as PNG, exit", "path"}); parser.process(app); MinimalWindow window; @@ -60,6 +62,9 @@ int main(int argc, char* argv[]) { if (parser.isSet("benchmark")) { window.setPendingBenchmark(parser.value("benchmark").toInt()); } + if (parser.isSet("screenshot")) { + window.setPendingScreenshot(parser.value("screenshot")); + } return app.exec(); } diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 4055d9b10c..d13408d8ef 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -1638,6 +1639,12 @@ void ViewportWindow::setBenchmarkFrames(int n) { requestUpdate(); } +void ViewportWindow::captureNextFrameToPng(const QString& path, bool quit_after) { + pending_screenshot_path_ = path; + pending_screenshot_quit_ = quit_after; + requestUpdate(); +} + QString ViewportWindow::cameraString() const { return QString("%1,%2,%3,%4,%5,%6") .arg(camera_target_.x(), 0, 'f', 4) @@ -2865,6 +2872,41 @@ void ViewportWindow::render() { buildHizPyramid(); } + // ---- One-shot framebuffer capture (parity-diff harness) ------------- + // Read back the just-rendered colour buffer and write a PNG. Runs + // before swapBuffers so the source is the back buffer (still bound). + // glReadPixels here is synchronous and at full surface size — fine + // for a one-shot capture, never used per-frame. + if (!pending_screenshot_path_.isEmpty() && gl_) { + const int w = int(width() * devicePixelRatio()); + const int h = int(height() * devicePixelRatio()); + if (w > 0 && h > 0) { + std::vector buf(size_t(w) * size_t(h) * 4); + gl_->glPixelStorei(GL_PACK_ALIGNMENT, 1); + gl_->glReadBuffer(GL_BACK); + gl_->glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, buf.data()); + // glReadPixels returns bottom-up; QImage is top-down — flip rows. + QImage img(w, h, QImage::Format_RGBA8888); + for (int y = 0; y < h; ++y) { + std::memcpy(img.scanLine(h - 1 - y), + buf.data() + size_t(y) * size_t(w) * 4, + size_t(w) * 4); + } + if (img.save(pending_screenshot_path_, "PNG")) { + qInfo().noquote() << "[gl] saved screenshot:" + << pending_screenshot_path_ + << "(" << w << "x" << h << ")"; + } else { + qWarning().noquote() << "[gl] QImage::save failed for" + << pending_screenshot_path_; + } + } + const bool quit_after = pending_screenshot_quit_; + pending_screenshot_path_.clear(); + pending_screenshot_quit_ = false; + if (quit_after) QCoreApplication::quit(); + } + context_->swapBuffers(this); // Ensure one more frame runs after the last motion frame so the diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index 5cb00b7447..14fe125b77 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -399,6 +399,14 @@ public: void setCamera(float tx, float ty, float tz, float dist, float yaw, float pitch); void setBenchmarkFrames(int n); + + // Queue a one-shot framebuffer capture: at the end of the next render() + // (just before swapBuffers), the default framebuffer is read back with + // glReadPixels, saved as PNG to `path`, and — if `quit_after` — the + // QCoreApplication is asked to quit. Used by parity-diff harnesses to + // produce a GL output PNG that's directly comparable to the wgpu + // backend's --screenshot. + void captureNextFrameToPng(const QString& path, bool quit_after = true); QString cameraString() const; // Move camera_target_ to the selected set's world-AABB centroid and @@ -716,6 +724,10 @@ private: float benchmark_yaw_speed_ = 0.5f; // degrees per frame std::vector benchmark_frame_times_; + // Queued one-shot framebuffer capture (see captureNextFrameToPng). + QString pending_screenshot_path_; + bool pending_screenshot_quit_ = false; + // Per-frame stats uint32_t visible_triangles_ = 0; uint32_t visible_objects_ = 0;