diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp index bb2475a37f..81323072ad 100644 --- a/src/ifcviewer/ViewportCore.cpp +++ b/src/ifcviewer/ViewportCore.cpp @@ -4603,3 +4603,111 @@ void ViewportCore::captureNextFrameToPng(const std::string& path, pending_screenshot_quit_ = quit_after; host_->requestFrame(); } + +// =========================================================================== +// Screenshot capture encode + finalize (#84-w) +// =========================================================================== + +WGPUBuffer ViewportCore::encodeScreenshotCapture( + WGPUCommandEncoder enc, WGPUTexture surface_texture, + std::uint32_t& padded_bpr_out) { + constexpr std::uint64_t kWgpuBytesPerRowAlign = 256; + const std::uint32_t row_bytes_unpadded = std::uint32_t(configured_w_) * 4u; + const std::uint32_t padded_bpr = std::uint32_t( + (row_bytes_unpadded + kWgpuBytesPerRowAlign - 1) + / kWgpuBytesPerRowAlign * kWgpuBytesPerRowAlign); + const std::uint64_t total_bytes = + std::uint64_t(padded_bpr) * std::uint64_t(configured_h_); + + WGPUBufferDescriptor bdesc = {}; + bdesc.size = total_bytes; + bdesc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead; + bdesc.label = svFromCStr("ifcviewer-wgpu.capture"); + WGPUBuffer capture_buffer = wgpuDeviceCreateBuffer(device_, &bdesc); + + WGPUTexelCopyTextureInfo src = {}; + src.texture = surface_texture; + src.aspect = WGPUTextureAspect_All; + + WGPUTexelCopyBufferInfo dst = {}; + dst.buffer = capture_buffer; + dst.layout.bytesPerRow = padded_bpr; + dst.layout.rowsPerImage = std::uint32_t(configured_h_); + + WGPUExtent3D extent = {}; + extent.width = std::uint32_t(configured_w_); + extent.height = std::uint32_t(configured_h_); + extent.depthOrArrayLayers = 1; + + wgpuCommandEncoderCopyTextureToBuffer(enc, &src, &dst, &extent); + + padded_bpr_out = padded_bpr; + return capture_buffer; +} + +void ViewportCore::finalizeScreenshotCapture(WGPUBuffer capture_buffer, + std::uint32_t padded_bpr) { + if (!capture_buffer) { + pending_screenshot_path_.clear(); + pending_screenshot_quit_ = false; + return; + } + + struct MapReq { bool done = false; bool ok = false; }; + MapReq req; + WGPUBufferMapCallbackInfo mcb = {}; + mcb.mode = WGPUCallbackMode_AllowProcessEvents; + mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView /*message*/, + void* ud1, void* /*ud2*/) { + auto* r = static_cast(ud1); + r->done = true; + r->ok = (status == WGPUMapAsyncStatus_Success); + if (!r->ok) Log::warn() << "wgpu MapAsync failed for screenshot"; + }; + mcb.userdata1 = &req; + + const std::uint64_t total_bytes = + std::uint64_t(padded_bpr) * std::uint64_t(configured_h_); + wgpuBufferMapAsync(capture_buffer, WGPUMapMode_Read, + 0, std::size_t(total_bytes), mcb); + while (!req.done) wgpuInstanceProcessEvents(instance_); + + if (req.ok) { + const std::uint8_t* mapped = static_cast( + wgpuBufferGetConstMappedRange(capture_buffer, 0, std::size_t(total_bytes))); + + // Assemble tightly-packed RGBA8. Surface is BGRA8 on most + // backends (BGRA8Unorm = format 28). If a future surface is + // already RGBA, skip the per-pixel swap. + const bool is_bgra = + surface_format_ == WGPUTextureFormat_BGRA8Unorm || + surface_format_ == WGPUTextureFormat_BGRA8UnormSrgb; + const std::uint32_t w = std::uint32_t(configured_w_); + const std::uint32_t h = std::uint32_t(configured_h_); + std::vector rgba(std::size_t(w) * std::size_t(h) * 4); + for (std::uint32_t y = 0; y < h; ++y) { + const std::uint8_t* src_row = mapped + std::size_t(y) * padded_bpr; + std::uint8_t* dst_row = rgba.data() + std::size_t(y) * std::size_t(w) * 4; + if (is_bgra) { + for (std::uint32_t x = 0; x < w; ++x) { + dst_row[x * 4 + 0] = src_row[x * 4 + 2]; // R <- B + dst_row[x * 4 + 1] = src_row[x * 4 + 1]; // G + dst_row[x * 4 + 2] = src_row[x * 4 + 0]; // B <- R + dst_row[x * 4 + 3] = src_row[x * 4 + 3]; // A + } + } else { + std::memcpy(dst_row, src_row, std::size_t(w) * 4); + } + } + wgpuBufferUnmap(capture_buffer); + + host_->saveScreenshotRgba8(pending_screenshot_path_, + rgba.data(), int(w), int(h)); + } + wgpuBufferRelease(capture_buffer); + + const bool quit_after = pending_screenshot_quit_; + pending_screenshot_path_.clear(); + pending_screenshot_quit_ = false; + if (quit_after) host_->quit(); +} diff --git a/src/ifcviewer/ViewportCore.h b/src/ifcviewer/ViewportCore.h index d315d7eb91..8088c987c5 100644 --- a/src/ifcviewer/ViewportCore.h +++ b/src/ifcviewer/ViewportCore.h @@ -338,6 +338,23 @@ public: void captureNextFrameToPng(const std::string& path, bool quit_after); bool pending_screenshot_quit_ = false; + // Encode a surface-to-buffer copy of the swapchain texture into the + // supplied encoder. Returns the allocated readback buffer (caller + // releases) and writes the row-aligned bytes-per-row through + // `padded_bpr_out`. The caller is expected to wait for queue submit + // before calling finalizeScreenshotCapture below. + WGPUBuffer encodeScreenshotCapture(WGPUCommandEncoder enc, + WGPUTexture surface_texture, + std::uint32_t& padded_bpr_out); + + // After queue submit, map the staging buffer back to host memory, + // BGRA→RGBA-swap into a tightly-packed RGBA8 image, hand it to + // host_->saveScreenshotRgba8, then release the staging buffer. + // Clears pending_screenshot_path_ and (when pending_screenshot_quit_ + // was set) invokes host_->quit(). + void finalizeScreenshotCapture(WGPUBuffer capture_buffer, + std::uint32_t padded_bpr); + // ---- Surface configuration (#84-u) ------------------------------------ // // Configure the swapchain at the given physical size. Picks a present diff --git a/src/ifcviewer/ViewportHost.h b/src/ifcviewer/ViewportHost.h index 8c33515936..97db91e853 100644 --- a/src/ifcviewer/ViewportHost.h +++ b/src/ifcviewer/ViewportHost.h @@ -45,6 +45,7 @@ #include #include +#include class ViewportHost { public: @@ -96,6 +97,16 @@ public: // existing typed signal and this is just the placeholder. virtual void onFrameStats(double /*frame_ms*/, double /*cull_ms*/, uint32_t /*draws*/, uint32_t /*tris*/) {} + + // Encode + save the screenshot. Called from the render path after + // wgpu has mapped the surface-copy staging buffer back to host + // memory; the host gets tightly-packed RGBA8 (BGRA→RGBA already + // swapped in core) at `w × h` and writes a PNG to `path`. Default + // body is a no-op so headless / web hosts can selectively ignore + // it — the QtViewportHost override calls QImage::save. + virtual void saveScreenshotRgba8(const std::string& /*path*/, + const std::uint8_t* /*rgba*/, + int /*w*/, int /*h*/) {} }; #endif // VIEWPORTHOST_H diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 2d8d2b91f1..575d168d1b 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -391,6 +391,22 @@ void ViewportWindow::onToolBackspacePressed() { emit toolBackspacePressed(); } +void ViewportWindow::saveScreenshotRgba8(const std::string& path, + const std::uint8_t* rgba, + int w, int h) { + // QImage takes a stride argument so it doesn't try to read past the + // last row — wgpu's staging buffer was BGRA8 padded; core already + // packed the rows tightly into rgba. + QImage img(rgba, w, h, w * 4, QImage::Format_RGBA8888); + const QString qpath = QString::fromStdString(path); + if (img.save(qpath, "PNG")) { + Log::info() << "[wgpu] saved screenshot: " + << path << " (" << w << "x" << h << ")"; + } else { + Log::warn() << "[wgpu] QImage::save failed for " << path; + } +} + void ViewportWindow::setBackgroundColor(float r, float g, float b, float a) { background_color_ = {r, g, b, a}; if (isExposed()) requestUpdate(); @@ -1783,37 +1799,12 @@ void ViewportWindow::render() { } // ---- Optional capture: encode copy on the same command buffer ------- - WGPUBuffer capture_buffer = nullptr; + WGPUBuffer capture_buffer = nullptr; uint32_t capture_padded_bpr = 0; const bool want_capture = !pending_screenshot_path_.empty(); if (want_capture) { - const uint32_t row_bytes_unpadded = uint32_t(configured_w_) * 4u; - capture_padded_bpr = uint32_t( - (row_bytes_unpadded + WGPU_BYTES_PER_ROW_ALIGN - 1) - / WGPU_BYTES_PER_ROW_ALIGN * WGPU_BYTES_PER_ROW_ALIGN); - const uint64_t total_bytes = uint64_t(capture_padded_bpr) * uint64_t(configured_h_); - - WGPUBufferDescriptor bdesc = {}; - bdesc.size = total_bytes; - bdesc.usage = WGPUBufferUsage_CopyDst | WGPUBufferUsage_MapRead; - bdesc.label = svFromCStr("ifcviewer-wgpu.capture"); - capture_buffer = wgpuDeviceCreateBuffer(device_, &bdesc); - - WGPUTexelCopyTextureInfo src = {}; - src.texture = surf_tex.texture; - src.aspect = WGPUTextureAspect_All; - - WGPUTexelCopyBufferInfo dst = {}; - dst.buffer = capture_buffer; - dst.layout.bytesPerRow = capture_padded_bpr; - dst.layout.rowsPerImage = uint32_t(configured_h_); - - WGPUExtent3D extent = {}; - extent.width = uint32_t(configured_w_); - extent.height = uint32_t(configured_h_); - extent.depthOrArrayLayers = 1; - - wgpuCommandEncoderCopyTextureToBuffer(enc, &src, &dst, &extent); + capture_buffer = core_.encodeScreenshotCapture( + enc, surf_tex.texture, capture_padded_bpr); } WGPUCommandBuffer cmd = wgpuCommandEncoderFinish(enc, nullptr); @@ -1823,72 +1814,10 @@ void ViewportWindow::render() { wgpuCommandEncoderRelease(enc); wgpuTextureViewRelease(view); - // ---- Optional capture: map + save PNG ------------------------------- - if (want_capture && capture_buffer) { - struct MapReq { bool done = false; bool ok = false; }; - MapReq req; - - WGPUBufferMapCallbackInfo mcb = {}; - mcb.mode = WGPUCallbackMode_AllowProcessEvents; - mcb.callback = [](WGPUMapAsyncStatus status, WGPUStringView message, - void* ud1, void* /*ud2*/) { - auto* r = static_cast(ud1); - r->done = true; - r->ok = (status == WGPUMapAsyncStatus_Success); - if (!r->ok) { - Log::warn().noquote() << "wgpu MapAsync failed:" << sv(message); - } - }; - mcb.userdata1 = &req; - - const uint64_t total_bytes = uint64_t(capture_padded_bpr) * uint64_t(configured_h_); - wgpuBufferMapAsync(capture_buffer, WGPUMapMode_Read, 0, size_t(total_bytes), mcb); - while (!req.done) wgpuInstanceProcessEvents(instance_); - - if (req.ok) { - const uint8_t* mapped = static_cast( - wgpuBufferGetConstMappedRange(capture_buffer, 0, size_t(total_bytes))); - - // Assemble tightly-packed RGBA8 image. Surface is BGRA8 on most - // backends (we saw format=28 = BGRA8Unorm), so swap R/B on the - // fly. If a future surface_format_ is RGBA8, just memcpy. - const bool is_bgra = - surface_format_ == WGPUTextureFormat_BGRA8Unorm || - surface_format_ == WGPUTextureFormat_BGRA8UnormSrgb; - const uint32_t w = uint32_t(configured_w_); - const uint32_t h = uint32_t(configured_h_); - QImage img(int(w), int(h), QImage::Format_RGBA8888); - for (uint32_t y = 0; y < h; ++y) { - const uint8_t* src_row = mapped + size_t(y) * capture_padded_bpr; - uint8_t* dst_row = img.scanLine(int(y)); - if (is_bgra) { - for (uint32_t x = 0; x < w; ++x) { - dst_row[x * 4 + 0] = src_row[x * 4 + 2]; // R <- B - dst_row[x * 4 + 1] = src_row[x * 4 + 1]; // G - dst_row[x * 4 + 2] = src_row[x * 4 + 0]; // B <- R - dst_row[x * 4 + 3] = src_row[x * 4 + 3]; // A - } - } else { - std::memcpy(dst_row, src_row, size_t(w) * 4); - } - } - wgpuBufferUnmap(capture_buffer); - - const QString qpath = QString::fromStdString(pending_screenshot_path_); - if (img.save(qpath, "PNG")) { - Log::info().noquote() << "[wgpu] saved screenshot: " - << pending_screenshot_path_ << " (" << w << "x" << h << ")"; - } else { - Log::warn().noquote() << "[wgpu] QImage::save failed for " - << pending_screenshot_path_; - } - } - wgpuBufferRelease(capture_buffer); - - const bool quit_after = pending_screenshot_quit_; - pending_screenshot_path_.clear(); - pending_screenshot_quit_ = false; - if (quit_after) QCoreApplication::quit(); + // Map the staging buffer back, BGRA→RGBA swap, hand to the host's + // saveScreenshotRgba8 (QImage::save on desktop, stb_image_write on web). + if (want_capture) { + core_.finalizeScreenshotCapture(capture_buffer, capture_padded_bpr); } // Emit per-frame stats before present so external listeners (bonsai's diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index df10b53cd0..11374cf42d 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -88,6 +88,8 @@ public: void onSurfacePickedInTool(int x_px, int y_px, int modifiers) override; void onToolModeChanged(int tool_mode) override; void onToolBackspacePressed() override; + void saveScreenshotRgba8(const std::string& path, const std::uint8_t* rgba, + int w, int h) override; void setBackgroundColor(float r, float g, float b, float a = 1.0f);