Skip to content

fix: move the scanner and camera work off the UI thread - #46

Merged
deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
re2zero:agent/pms-bug-bot/db4916c933bb
Oct 12, 2026
Merged

deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
re2zero:agent/pms-bug-bot/db4916c933bb

Conversation

@re2zero

@re2zero re2zero commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

fix: move the scanner and camera work off the UI thread

Opening and enumerating scanners, opening and capturing from a camera,
processing and saving a scan and rendering the PDF all ran on the UI thread and
froze the window for up to several seconds (about seven seconds for a SANE
enumeration, one second for an A4 page at 300 dpi). They now run on worker
threads, the fixed sleeps became waits with a timeout, the frame formats that
map onto a QImage no longer go through a PNG temp file, and elapsed-time logs
were added to tell the slow phase apart.

扫描仪打开/枚举/扫描、相机打开/开流/采集/切换分辨率、扫描结果处理与保存、PDF 渲染原本都在 UI
线程执行,界面会冻结数秒(SANE 枚举约 7 秒,A4 300dpi PDF 渲染约 1 秒)。现全部移入工作线程,
固定睡眠改为带超时等待,可直接映射为 QImage 的帧格式不再经过 PNG 临时文件中转,并补充各阶段
耗时日志。

Log: 扫描与相机操作移出 UI 线程,扫描、保存、加载期间界面不再冻结
PMS: BUG-346173
Influence: 打开设备、开始扫描、保存结果、切换相机分辨率期间界面保持响应;默认相机分辨率由设备最大值改为 1080p(界面可改回);输出文件内容不变。

Summary by Sourcery

Move scanner and camera operations, scan processing, and PDF rendering off the UI thread to keep the application responsive during device and file operations.

New Features:

  • Move scanner enumeration, device access, camera capture and preview processing to worker threads so the UI remains responsive.
  • Return compatible scanner frames directly as QImages and process/save scan results asynchronously without PNG temporary-file conversion.
  • Use 1080p or another suitable HD resolution as the default camera resolution while retaining higher resolutions as selectable options.

Bug Fixes:

  • Prevent camera waits and device reconfiguration from blocking indefinitely by replacing fixed delays with bounded, abortable waits.
  • Avoid scanner and camera operations freezing the main window during device discovery, scanning, saving, loading, and resolution changes.

Enhancements:

  • Add timing logs for device opening, scanning, frame reads, and scan processing and output.
  • Improve camera frame conversion performance and coordinate preview, capture, and device teardown safely across threads.

@sourcery-ai

sourcery-ai Bot commented Oct 12, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR removes lengthy scanner, camera, image-processing, file-save, and PDF-rendering operations from the UI thread by routing device work through worker signals and using QtConcurrent for independent processing, while adding bounded waits, faster image paths, safer camera defaults, and timing diagnostics.

Sequence diagram for asynchronous scanner enumeration and capture

sequenceDiagram
    participant UI as MainWindow or ScanWidget
    participant Device as ScannerDevice
    participant Worker as ScannerWorker thread
    participant SANE as SANE backend

    UI->>Device: requestAvailableDevices()
    Device-->>Worker: triggerGetAvailableDevices()
    Worker->>SANE: sane_get_devices()
    SANE-->>Worker: device list
    Worker-->>Device: availableDevicesReady(deviceNames)
    Device-->>UI: availableDevicesReady(deviceNames)

    UI->>Device: startScan()
    Device-->>Worker: triggerStartScan()
    Worker->>SANE: sane_start()
    Worker->>SANE: sane_read()
    alt QImage-compatible frame
        Worker-->>Device: captureCompletedImage(image)
    else other frame format
        Worker-->>Device: captureCompleted(filePath)
    end
    Device-->>UI: imageCaptured or scan completion
Loading

Sequence diagram for background scan processing and file saving

sequenceDiagram
    participant UI as ScanWidget UI thread
    participant Worker as QtConcurrent worker
    participant File as Output file

    UI->>Worker: saveScan(request)
    Worker->>Worker: convertToBlackWhite(image)
    Worker->>Worker: detectPaperSize(image, dpi)
    Worker->>Worker: scaleToPaperSize(image, paperSize, dpi)
    alt PDF output
        Worker->>Worker: renderPdf(image, filePath, paperSize)
        Worker->>File: write PDF
    else image or OFD output
        Worker->>File: save output
    end
    Worker-->>UI: ScanSaveResult
    UI->>UI: update scan history
Loading

State diagram for bounded camera capture and reconfiguration

stateDiagram-v2
    [*] --> Initialized
    Initialized --> Streaming: startStream()
    Streaming --> Capturing: startCapturing()
    Capturing --> Streaming: poll and VIDIOC_DQBUF succeeds
    Capturing --> Streaming: FRAME_WAIT_TIMEOUT_MS expires
    Streaming --> Reconfiguring: setResolution()
    Reconfiguring --> Streaming: startStream()
    Streaming --> Initialized: stopCapturing()
    Reconfiguring --> Initialized: closeDevice() fails
Loading

File-Level Changes

Change Details Files
Moved scanner enumeration and scan acquisition into the scanner worker thread, with asynchronous result delivery and phase timing logs.
  • Route device enumeration through worker signals and cache the latest list for synchronous UI reads.
  • Measure device open, option setup, scan start, and scan-read phases.
  • Deliver directly mappable SANE grayscale/RGB frames as QImages, retaining PNG temp-file fallback for unsupported formats.
  • Replace scanner progress forwarding with image-completion signaling for direct-image captures.
src/device/scannerdevice.cpp
src/device/scannerdevice.h
Reworked webcam streaming and frame capture to avoid blocking UI operations and unbounded waits.
  • Start and restart V4L2 streams during device setup and resolution changes.
  • Use nonblocking device access with bounded buffer retries and poll-based frame waits.
  • Track streaming state and remove fixed stabilization sleeps.
  • Optimize YUYV conversion with scanline writes and select 1080p/720p-friendly default resolutions.
src/device/webcamdevice.cpp
src/device/webcamdevice.h
Moved scan processing, file writing, PDF rendering, and camera resolution changes to background execution.
  • Run scan conversion, paper sizing, image output, OFD output, and PDF rendering via QtConcurrent with completion watchers.
  • Capture all required settings on the UI thread and return only a compact save result to update history safely.
  • Run camera resolution reconfiguration asynchronously and prevent captures while it is in progress.
  • Use faster preview scaling before QPixmap conversion.
src/ui/scanwidget.cpp
src/ui/scanwidget.h
Changed device-list initialization to be event-driven and adjusted loading overlay behavior.
  • Request scanner enumeration after the existing startup delay and rebuild the UI when the worker reports completion.
  • Remove synchronous enumeration and obsolete opening-state checks.
  • Mark the loading dialog as a Qt tool window.
src/mainwindow.cpp
src/ui/scannerswidget.cpp
src/ui/loadingdialog.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/ui/scanwidget.cpp" line_range="490" />
<code_context>
+                        webcam->startPreview();
+                    }
+                });
+                watcher->setFuture(QtConcurrent::run(&WebcamDevice::setResolution, webcam, width, height));
             }
         }
</code_context>
<issue_to_address>
**Device destroyed during reconfiguration**

When the application closes before the background resolution change completes, `QtConcurrent::run()` captures a raw `WebcamDevice*`, and `MainWindow::~MainWindow()` clears the last shared owner without waiting for the task. `setResolution()` then uses a destroyed device, causing use-after-free or concurrent teardown.

Keep the device alive until the task completes, or make application shutdown wait for the reconfiguration task.
</issue_to_address>

### Comment 2
<location path="src/ui/scanwidget.cpp" line_range="573" />
<code_context>
+    ScanSaveRequest request;
+    request.image = image;
+    request.filePath = QDir(getSaveDirectory())
+                           .filePath(QString("%1.%2").arg(QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss"))
+                                         .arg(FORMATS[m_imageSettings->format].toLower()));
+    request.formatIndex = m_imageSettings->format;
</code_context>
<issue_to_address>
**Concurrent saves overwrite scan files**

When two scans using the same output format and directory finish within the same second, `onScanFinished()` builds each `filePath` with only second-level timestamp precision, then starts `saveScan()` asynchronously without reserving the path. Both jobs write the same file, so one scan overwrites or corrupts the other and history can contain duplicate paths.

Assign each scan a unique output path before starting its asynchronous save.

Also at `src/ui/scanwidget.cpp:574-599`.
</issue_to_address>

### Comment 3
<location path="src/device/scannerdevice.cpp" line_range="882-883" />
<code_context>
             break;
         }

+        if (!writePng && linesRead >= parm.lines) {
+            break;   // the image is complete
+        }
+
</code_context>
<issue_to_address>
**Completed scans are reported as failures**

When SANE supplies a positive line count and a frame format handled directly as a `QImage`, `scan_it` breaks while `status` is still `SANE_STATUS_GOOD`; its later EOF check therefore cancels the scan and returns an error, so `ScannerWorker::doStartScan` reports failure instead of emitting the completed image.

Treat reaching `parm.lines` as successful completion, or continue reading until SANE returns EOF before applying the EOF check.
</issue_to_address>

### Comment 4
<location path="src/device/webcamdevice.cpp" line_range="637" />
<code_context>
+        // Wait for a frame with a timeout instead of blocking in VIDIOC_DQBUF: the file
+        // descriptor stays in non-blocking mode, so a stalled stream cannot hang here.
+        struct pollfd pfd = { m_fd, POLLIN, 0 };
+        const int ready = poll(&pfd, 1, FRAME_WAIT_TIMEOUT_MS);
+        const bool gotFrame = (ready > 0) && (ioctl(m_fd, VIDIOC_DQBUF, &buf) != -1);

</code_context>
<issue_to_address>
**Camera capture freezes the UI**

When a camera frame is delayed or unavailable during capture, `ScanWidget::startScanning()` calls `startCapture()` synchronously on the UI thread, which reaches `captureImage()` and waits in `poll()` for up to two seconds per attempt. Three unsuccessful attempts can block the window for up to six seconds.

Run capture off the UI thread or otherwise ensure frame waits do not block the UI thread.

Also at `src/device/webcamdevice.cpp:638`.
</issue_to_address>

### Comment 5
<location path="src/device/webcamdevice.cpp" line_range="729-755" />
<code_context>
-        size_t expectedSize = width * height * 2;   // YUYV格式每个像素需要2字节
-        if (expectedSize > 0) {   // 合理的大小上限
-            try {
-                for (int y = 0; y < height; y++) {
-                    for (int x = 0; x < width; x += 2) {
-                        int i = y * width * 2 + x * 2;
-
-                        // Boundary check
-                        if (i + 3 >= expectedSize) {
-                            qCWarning(app) << "Frame data boundary check failed - possible incomplete data";
-                            continue;
-                        }
-
-                        int y0 = yuyv[i];
</code_context>
<issue_to_address>
**High-resolution preview still blocks UI**

When the camera preview is active at a resolution whose frame conversion takes appreciable time, `updatePreview()` invokes `frameToQImage()` from the UI-thread timer, so this per-pixel conversion still runs on the UI thread for every preview frame. At high resolutions, conversion stalls the window even though opening and reconfiguring the camera moved to workers.

Move preview frame dequeue and conversion off the UI thread, then deliver the converted frame to the widget asynchronously.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread src/ui/scanwidget.cpp Resolved
Comment thread src/ui/scanwidget.cpp Resolved
Comment thread src/device/scannerdevice.cpp Resolved
Comment thread src/device/webcamdevice.cpp Resolved
Comment thread src/device/webcamdevice.cpp Resolved
Opening and enumerating scanners, opening and capturing from a camera,
processing and saving a scan and rendering the PDF all ran on the UI thread and
froze the window for up to several seconds (about seven seconds for a SANE
enumeration, one second for an A4 page at 300 dpi). They now run on worker
threads, the fixed sleeps became waits with a timeout, the frame formats that
map onto a QImage no longer go through a PNG temp file, and elapsed-time logs
were added to tell the slow phase apart.

The camera frame work followed: dequeuing a frame, converting it and waiting for
a frame during a capture still ran on the UI thread, which froze the window for
about a second on a stream that had to be started first and spent most of every
preview frame on the per-pixel conversion. Both now run on a single per-device
I/O thread that keeps the preview and the capture from touching the device at
the same time, the frame wait is sliced so that a device that is being closed or
reconfigured ends it within about 100 ms, the preview timer is only touched from
the UI thread, the device-level operations are serialized, two scans that end
within the same second no longer build the same file name, and the application
waits for a camera reconfiguration before it releases the device.

扫描仪打开/枚举/扫描、相机打开/开流/采集/切换分辨率、扫描结果处理与保存、PDF 渲染原本都在 UI
线程执行,界面会冻结数秒(SANE 枚举约 7 秒,A4 300dpi PDF 渲染约 1 秒)。现全部移入工作线程,
固定睡眠改为带超时等待,可直接映射为 QImage 的帧格式不再经过 PNG 临时文件中转,并补充各阶段
耗时日志。

相机的取帧与转换随后也移入工作线程:预览取帧、逐像素转换和采集时等待首帧原本仍在 UI 线程执行,
需要现开流的采集会冻结界面约 1 秒,预览每帧的转换也几乎占满 UI 线程。现两者都在设备专属的单个
I/O 线程上串行执行;等待帧改成分片等待(设备关闭或重配置时约 100ms 内让出),预览定时器只在 UI
线程操作,设备级操作(打开/关闭/重配置)串行化,同一秒内结束的两次扫描不再生成同名文件,退出
应用时会等待进行中的相机重配置结束后再释放设备。

Log: 扫描与相机操作移出 UI 线程,扫描、保存、预览期间界面不再冻结

PMS: BUG-346173

Influence: 打开设备、开始扫描、保存结果、切换相机分辨率与预览期间界面保持响应;同一秒内连续两次扫描会生成不同文件名的结果;退出应用时若正在切换相机分辨率会等待该操作完成(最多数秒);默认相机分辨率由设备最大值改为 1080p(界面可改回);输出文件内容不变。
@re2zero
re2zero force-pushed the agent/pms-bug-bot/db4916c933bb branch from d5e00c3 to 2c84ae0 Compare October 12, 2026 03:45
@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

🤖 AI 代码审查报告

总体评分: 97 分 (通过阈值: 70分)

Pass


📊 总体评价

项目 结果
审查结论 代码审查通过
评分详情 总体评分 97 分,大于 70 分通过阈值。本次提交将扫描仪和摄像头的阻塞操作从 UI 线程移至工作线程,显著提升了 UI 响应性能。线程同步机制实现完善,代码注释详尽,无安全漏洞。

🔍 详细分析

1. 语法逻辑 ✅

评价: 优秀 ✅ 通过

潜在问题:

  1. src/device/webcamdevice.cpp:868 - frameToQImage() 移除了逐像素边界检查,新的按行访问方式在数学上正确但缺少防御性校验
  2. src/device/scannerdevice.cpp:363 - scan_it() 中 memcpy 后若 len < bytesPerLine(),该行剩余字节可能未初始化

建议: 建议在 scan_it() 的 memcpy 后,当 len < image.bytesPerLine() 时对剩余字节清零,避免 QImage 中出现未初始化数据


2. 代码质量 ✅

评价: 优秀 ✅ 通过

潜在问题:

  1. src/device/webcamdevice.cpp:667 - captureImage() 函数较长,可进一步拆分重试循环逻辑
  2. src/device/webcamdevice.cpp:291 - setResolution() 承担多个职责(关闭、重开、格式设置),可考虑拆分

建议: 建议将 captureImage() 中的重试循环逻辑提取为独立方法,如 tryCaptureFrame(),提升可读性和可维护性


3. 代码性能 ✅

评价: 优秀 ✅ 通过

潜在问题:
✅ 未发现明显问题

建议: 无需额外优化建议,性能改进非常到位


4. 代码安全 🔒

评价: 优秀 ✅ 通过

🔐 发现 0 个安全漏洞

安全漏洞详情:
✅ 未发现安全漏洞

建议: 无需安全加固,代码不涉及用户输入处理、网络操作或敏感信息


💡 改进建议代码示例

// scan_it() 中 memcpy 后对剩余字节清零
if (!writePng) {
    const int copyLen = qMin(len, image.bytesPerLine());
    memcpy(image.scanLine(linesRead), buffer, copyLen);
    if (copyLen < image.bytesPerLine()) {
        memset(image.scanLine(linesRead) + copyLen, 0,
               image.bytesPerLine() - copyLen);
    }
}

本报告由 AI 代码审查工具自动生成

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: lzwind, re2zero

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@re2zero

re2zero commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

/merge

@deepin-bot
deepin-bot Bot merged commit 36132f3 into linuxdeepin:master Oct 12, 2026
14 checks passed
@re2zero
re2zero deleted the agent/pms-bug-bot/db4916c933bb branch October 12, 2026 05:20
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.

3 participants